【问题标题】:"immutable" struct with non-trivial constructor具有非平凡构造函数的“不可变”结构
【发布时间】:2016-06-12 02:36:39
【问题描述】:

我一直在寻找一种使用非平凡构造函数声明某种不可变类型的方法。我目前的目标是从文件中读取数据以构造一个对象,以便随后无法对其进行修改。它类似于 POD 类型,除了我需要文件中的数据,因此构造函数必须读取它。

通过我的研究和实验,我想到了三种方法来做到这一点。基本上,我的问题是:有没有更好的方法来做我想做的事?

在下面的示例代码中,我将使用std::cin 作为文件的替代品。首先,这是明显的类与 getters 方式:

class A {
public:
    A() { std::cin >> m_i; }
    int i() { return m_i; }

private:
    int m_i;
};

事实上,我在使用这个解决方案时遇到了麻烦,仅仅是因为 getter(s)。毕竟,它是一种 POD 类型,我希望它被这样对待,具有公共数据成员。另外,我只是不喜欢吸气剂。所以我尝试了一些const-ness 并调整了构造函数:

struct B {
    B() : B(B::fromFile()) {
    }

    B(int i) : i(i) {
    }

    const int i;

private:
    static B fromFile() {
        int i;
        std::cin >> i;
        return B(i);
    }
};

这里有几个问题。我需要委托给静态方法,因为我无法直接在构造函数的初始化列表中获取成员的值。该方法需要为每个成员(这里只是i)创建一个副本并分别初始化它们,以便在使用复制构造函数最终构造初始对象之前将它们传递给另一个构造函数。此外,由于新的构造函数和静态方法,它需要更多的代码行。

因此,这种方法似乎注定要失败。然后我意识到,我真正想要的是该类/结构的每个实例都是const。但是,据我所知,没有办法强制用户每次都使用const 关键字。所以,我想到了using 别名声明。有点像标准库为const_reference 所做的事情(几乎在每个容器中)。只有在这种情况下,它才会反过来:类型将被称为NonConstType,或者我们说MutableType,别名将被声明为:

using Type = const MutableType;

因为我不想污染命名空间,所以我们使用Mutable 命名空间。下面是代码的样子:

namespace Mutable {
    struct C {
        C() { std::cin >> i; }

        int i;
    };
}

using C = const Mutable::C;

这样,我可以提供一个“不可变”类,它可以像 C 结构(没有 getter)一样处理,但仍然可以使用来自不同文件的数据来构造。此外,可变版本仍然可用,我认为这毕竟是一件好事。

那么,还有其他方法吗?在这三个代码中的任何一个中,是否有我没有考虑到的优点或缺点?

A full testing code can be found here.

【问题讨论】:

  • const 类型不能移动,只能复制。
  • C 是您想要的,除了在不同的函数中读取并将最终参数传递给构造函数。而且每次声明一个 const 对象时不使用using C,写const 会更易读。
  • @M.M. C 类型是不可变的,但 OP 声明强制复制是不可取的,C 类型强制这样做。
  • @Cheersandhth.-Alf 我在帖子中没有看到任何关于“强制复制”的内容(无论这应该是什么意思)。 OP 声明该类型应该是不可变的,我认为这意味着它是 const 一旦它被构造(故意导致它不能被移动)。
  • @M.M.当您从函数返回 const 类型的对象时,它不能被移动。所以const 强制复制。在某些情况下,这种复制可以通过返回值优化来消除。同样,当您通过值传递这样的对象时,特别是通过两个或多个级别的调用,除了这个复制不能被优化掉,除非通过非常积极的内联。您不会在帖子中找到“强制复制”一词,因为 (1) 这是一个逻辑结果,并且 (2) OP 不一定意识到这一点。

标签: c++ c++11


【解决方案1】:

您使用的是 C++11,那为什么不使用聚合初始化呢?

#include <iostream>

struct Foo {
    const int val; // Intentionally uninitialized.
};

struct Foo create_foo_from_stream (std::istream& stream) {
    int val;
    stream >> val;
    return Foo{val};
}

int main () {
    Foo foo (create_foo_from_stream(std::cin));
    std::cout << foo.val << '\n';
}

初始化struct Foo 的唯一方法是通过聚合初始化或复制构造。默认构造函数被隐式删除。

请注意,在 C++14 中,您可以使用默认成员初始化程序,但仍然使用聚合初始化:

#include <iostream>

struct Foo {
    const int val = 0;  // Prevents aggregate in C++11, but not in C++14.
};

struct Foo create_foo_from_stream (std::istream& stream) {
    int val;
    stream >> val;
    return Foo{val};
}

int main () {
    Foo foo (create_foo_from_stream(std::cin));
    Foo bar;   // Valid in C++14.
    std::cout << foo.val << bar.val << '\n';
}

【讨论】:

  • public const 成员不服从const_cast吗?
  • 在实例化类型时,我对调用另一个函数而不是构造函数感到有些不安。这就是为什么我尝试在我的B 结构中只提供构造函数。但我想这个解决方案毕竟是一个很好的解决方案。你会推荐它而不是我提到的最后一个(C 结构)吗?
  • @Nelxost -- 具有用户提供的构造函数的类不是aggregate。唯一能让你的结构 C 不被聚合的是构造函数。初始化此类类的唯一方法是调用已定义的构造函数之一或未删除的复制或移动构造函数。聚合初始化非常方便。
  • @bipll -- “那就不要那样做!”这同样适用于问题中定义的类。 const_cast 的好处是它的使用是可搜索的——并且在代码审查期间受到质疑。 C 风格的强制转换很难通过人工审查来根除,但编译器非常擅长找到它们。
  • 好吧,我最终在您的解决方案和 Mark B 之间使用了一些混合。虽然,由于我在单独的函数中读取文件的方式,我正在标记您的答案。
【解决方案2】:

如果只使用模板读取助手呢?您不需要它是静态的或类成员,只需一个免费模板即可从流中提取正确的值。

#include <iostream>

template <typename T>
T stream_read(std::istream& is)
{
    T val;
    is >> val;

    return val;
}

struct B
{
    B() : i_(stream_read<int>(std::cin)) { }

    const int i_;
};

int main()
{
    B b;

    std::cout << "value=" << b.i_ << std::endl;
}

【讨论】:

  • 我觉得模板很有趣,但正如我在对 max66 的回答的评论中所说的那样,它看起来过于严格。考虑到我需要从同一个文件初始化多个数据成员,我需要向该函数添加额外的参数,这会阻止我在读取文件时做其他事情(条件搜索示例)。
  • @Nelxost 你能详细说明为什么你需要额外的函数参数吗?我专门设置了它,因此您可以根据需要从文件中提取尽可能多或少量的数据。如果您在阅读文件时需要做其他事情,您应该在问题中明确说明。
  • 考虑一个可能包含“1 2 3”或“1 1 3”的文件。该结构有两个int 成员a 和b。我如何使用您的方法将 1 放入 a,在第一种情况下,将 2 放入 b,但在第二种情况下,将 3 放入 b(丢弃 1)?
  • @Nelxost 接受或拒绝文件中的值的规则是什么?
  • 我会重新制定。如果文件包含“1 2 3”,那么我想将第一个值 1 放入 a,第二个值 2 放入 b。如果文件包含“1 1 3”,那么我仍然想将第一个值1放入a,但我想丢弃第二个1并放入第三个值,3,进入b。那将是我想丢弃重复项的一个例子。不过,我目前拥有的是一个像字典一样格式化的文件,带有键和值。在第二种情况下,额外的参数将是寻找的关键。
【解决方案3】:

不变性的程度。

回复

我目前的目标是从文件中读取数据来构造一个对象,这样以后就不能修改了

有一定程度的不变性,例如:

  • 完全不可变
    这是旧的const,无论是对于类型还是对于单个数据成员。缺点:不能移动,例如,当用作函数返回值时会强制复制。但是,编译器可能会优化掉此类复制,并且通常会这样做。

  • 不可变但可移动
    即使编译器没有优化,这也允许有效的函数返回值。也非常适合将原始临时文件传递到底部函数存储副本的按值调用链:它可以一直移动。

  • 不可变但可移动且可复制分配
    Assignable 听起来可能与 immutable 的设计方向不同,实际上新手可能会认为这些属性直接冲突! Python 和 Java 字符串就是这样的例子:它们是不可变的,但可以赋值。本质上,这是封装句柄值方法的一种巧妙方法。用户代码处理句柄,但它似乎直接处理值,如果用户代码可以更改一个值,那么持有相同值句柄的用户代码的其他部分将看到变化,这与全局变量的意外变化一样不好。因此 values 需要是不可变的,但用户代码 objects 不需要(可以只是句柄)。

最后一点表明,逻辑设计级别需要将内部值与用户代码对象区分开来。

从这个观点来看,上面的第一点是关于值和对象都是不可变的;第二点具有不可变的值和通常不可变的对象,但允许从临时对象中高效且令人愉快的低级别窃取值,使它们在逻辑上为空;第三点具有不可变的值,但对象在复制分配和从临时对象移动方面都是可变的。

数据。

对于所有这三种可能性,我们可以定义一个简单的内部 Data 类,如下所示:

数据.hpp
#pragma once
#include "cppx.hpp"     // cppx::String, an alias for std::wstring

namespace my {
    using cppx::String;

    struct Data
    {
        String  name;
        int     birthyear;
    };
}  // namespace my

这里的 cppx.hpp 是一个小帮助文件,具有 ¹一般的便利功能,我在最后列出。

在您的实际用例中,数据类可能会有其他数据字段,但主要思想是它是一个简单的聚合类,只是数据。您可以将其视为对应于处理值方法中的“值”。接下来,让我们定义一个类作为用户代码变量的类型。

完全不可变的对象。

以下类实现了用户代码对象完全不可变的思想:初始化时设置的值根本无法更改,并且一直持续到对象销毁。

Person.all_const.hpp
#include "Data.hpp"     // my::(String, Data), cppx::*

namespace my {
    using cppx::int_from;
    using cppx::line_from;
    using cppx::In_stream;      // alias std::wistream

    class Person
    {
    private:
        Data const  data_;

    public:
        auto operator->() const noexcept
            -> Data const*
        { return &data_; }

        explicit Person( In_stream& stream )
        try
            : data_{ line_from( stream ), int_from( stream ) }
        {} CPPX_RETHROW_X
    };
}   // namespace my

这里

  • data_ 成员的const 提供所需的完全不变性;

  • operator-&gt; 可以轻松访问Data 字段;

  • operator-&gt; 上的 noexcept 可能会在某些方面帮助编译器,但主要是为了程序员的利益,即记录此访问器不会抛出;

  • 构造函数是explicit,因为在设计级别它不提供来自流参数的转换

  • line_fromint_from 的调用顺序以及流中行的使用顺序得到保证“因为这是花括号初始化列表;

  • line_fromint_from 函数是&lt;cppx.hpp&gt; 帮助器,它们分别从指定流中读取一行并尝试分别返回完整的行字符串,以及std::stoi 生成的int,失败时抛出异常;和

  • CPPX_RETHROW_X 宏获取函数名称并追溯异常,并将该名称附加到异常消息中,作为在异常中获取简单调用堆栈跟踪的原始显式方式。

可以定义一个名为data 的访问器方法来代替operator-&gt;,例如,返回一个Data const&amp;,但operator-&gt; 提供了一个非常好的使用语法,如下所示:

一个示例主程序。

main.cpp
#include PERSON_HPP         // E.g. "Person.all_const.hpp"
#include <iostream>
using namespace std;

auto person_from( cppx::In_stream& stream )
    -> my::Person
{ return my::Person{ stream }; }

void cppmain()
{
    auto x = person_from( wcin );   // Will not be moved with the const version.
    wcout << x->name << " (born " << x->birthyear << ").\n";

    // Note: due to the small buffer optimization a short string may not be moved,
    // but instead just copied, even if the machinery for moving is there.
    auto const x_ptr = x->name.data();
    auto y = move( x );
    bool const was_moved = (y->name.data() == x_ptr);
    wcout << "An instance was " << (was_moved? "" : "not ") << "moved.\n";
}

auto main() -> int { return cppx::mainfunc( cppmain ); }

这里的cppx::mainfunc 也是来自&lt;cppx.hpp&gt; 的助手,负责捕获异常并将其消息显示在std::wcerr 流上。

我使用宽流,因为这是支持 Windows 控制台程序的国际字符的最简单方法,而且它们也可以在 Unix 领域工作(至少当其中包括对 setlocale 的调用时,这也是由 cppx::mainfunc 完成的),因此它们实际上是最便携的选项:它们使这个示例最便携。 :)

最后的代码对于完全不可变的const 版本没有多大意义,所以让我们看一下可移动版本:

不可变但可移动的对象。

Person.movable.hpp
#include "Data.hpp"     // my::(String, Data), cppx::*
#include <utility>      // std::move

namespace my {
    using cppx::In_stream;
    using cppx::int_from;
    using cppx::line_from;
    using std::move;

    class Person
    {
    private:
        Data data_;

        auto operator=( Person const& ) = delete;
        auto operator=( Person&& ) = delete;

    public:
        auto operator->() const noexcept
            -> Data const*
        { return &data_; }

        explicit Person( In_stream& stream )
        try
            : data_{ line_from( stream ), int_from( stream ) }
        {} CPPX_RETHROW_X

        Person( Person&& other ) noexcept
            : data_{ move( other.data_ ) }
        {}
    };
}   // namespace my

请注意,需要明确指定移动构造函数,如上图末尾所示。

正如 g++ 解释的那样,如果不这样做,那么

my::Person::Person(const my::Person&amp;)' is implicitly declared as deleted because 'my::Person' declares a move constructor or move assignment operator

不可变但可移动和可分配的对象(一个漏洞!)。

要使对象可分配,只需删除= delete 声明即可。

但是这样自动移动构造函数不会被隐式删除,所以它的显式版本可以被删除,产生

Person.assignable.hpp
#pragma once
#include "Data.hpp"     // my::(String, Data), cppx::*
#include <utility>      // std::move

namespace my {
    using cppx::In_stream;
    using cppx::int_from;
    using cppx::line_from;

    class Person
    {
    private:
        Data data_;

    public:
        auto operator->() const noexcept
            -> Data const*
        { return &data_; }

        explicit Person( In_stream& stream )
        try
            : data_{ line_from( stream ), int_from( stream ) }
        {} CPPX_RETHROW_X
    };
}   // namespace my

这样更短更简单,很好。

但是,由于它支持复制分配,它允许修改实例x 的部分值。

怎么样?好吧,一种方法是从x 中复制完整的Data 值,修改Data 实例,用两行上的值格式化相应的字符串,使用它来初始化std::wistringstream,将该流传递给Person 构造函数,并将该实例分配回x。呸!多么迂回的黑客!但它表明,在理论上,它是可能的,而且效率很低,例如编写用于复制可分配Person 类的set_birthyear 函数。而这样的漏洞,类型中的安全漏洞,有时会产生问题。

不过,我只是为了完整性而提及该漏洞,以便人们能够意识到它——并且可能会意识到其他代码中的类似功能漏洞。而且我认为我个人会选择这个版本的Person 类。因为越简单,就越容易使用和维护。

为了完整性:上面使用的 cppx 支持。

cppx.hpp
#pragma once

#include <iostream>     // std::(wcerr, wistream)
#include <locale.h>     // setlocale, LC_ALL
#include <stdexcept>    // std::runtime_error
#include <string>       // std::(wstring, stoi)
#include <stdlib.h>     // EXIT_...

#ifndef CPPX_QUALIFIED_FUNCNAME
#   if defined( _MSC_VER )
#       define CPPX_QUALIFIED_FUNCNAME  __FUNCTION__
#   elif defined( __GNUC__ )
#       define CPPX_QUALIFIED_FUNCNAME  __PRETTY_FUNCTION__     // Includes signature.
#   else
#       define CPPX_QUALIFIED_FUNCNAME  __func__    // Unqualified but portable C++11.
#   endif
#endif

// Poor man's version, roughly O(n^2) in the number of stack frames unwinded.
#define CPPX_RETHROW_X \
    catch( std::exception const& x ) \
    { \
        cppx::fail( \
            cppx::Byte_string() + CPPX_QUALIFIED_FUNCNAME + " | " + x.what() \
            ); \
    }

namespace cppx {
    using std::endl;
    using std::exception;
    using std::runtime_error;
    using std::stoi;

    using String = std::wstring;
    using Byte_string = std::string;

    using In_stream     = std::wistream;
    using Out_stream    = std::wostream;

    struct Sys
    {
        In_stream& in       = std::wcin;
        Out_stream& out     = std::wcout;
        Out_stream& err     = std::wcerr;
    };

    Sys const sys = {};

    [[noreturn]]
    inline auto fail( Byte_string const& s )
        -> bool
    { throw runtime_error( s ); }

    inline auto line_from( In_stream& stream )
        -> String
    try
    {
        String result;
        getline( stream, result ) || fail( "getline" );
        return result;
    } CPPX_RETHROW_X

    inline auto int_from( In_stream& stream )
        -> int
    try
    {
        return stoi( line_from( stream ) );
    } CPPX_RETHROW_X

    inline auto mainfunc( void (&f)() )
        -> int
    {
        setlocale( LC_ALL, "" );    // E.g. for Unixland wide streams.
        try
        {
            f();
            return EXIT_SUCCESS;
        }
        catch( exception const& x )
        {
            sys.err << "! " << x.what() << endl;
        }
        return EXIT_FAILURE;
    }
}  // namespace cppx

¹ 我认为如果 Stack Overflow C++ 社区可以对这样的文件进行标准化,以减少阅读答案示例的认知负担,也可能是问题!但我认为大多数读者会发现 my(以及其他任何人的)助手乍一看非常陌生,其次,我懒得把这个想法带到 C++ Lounge 并在那里讨论,IMO 将是这样做的方式。
² 见(Order of evaluation of elements in list-initialization)。

【讨论】:

  • 感谢您的奉献!有趣的东西。但是,我选择了一个更简单的解决方案,我认为它更适合我的代码。
  • @Nelxost:您选择作为解决方案的答案 A 与上面完全不可变的对象之间的主要区别是 (1) A 使用工厂函数而不是构造函数,contrary to your wishes; (2) A 不进行错误检查,并且 (3) A 不分解数据表示。现在阅读链接后的评论,我发现解析文本文件有一些未说明且非常不清楚的要求。抱歉没看到。
  • 嗯,这些答案让我意识到我真正需要的东西是不可能的,或者是非常不切实际的代码。所以我选择了某种工厂函数,除了我使用了一个中间函数,以便我的类不处理文件(或流),而是处理预先从文件中提取的格式化数据。这样,我仍然使用我的类的构造函数,但我不直接给它文件。可能是我描述的不够清楚,抱歉。我应该编辑我的问题(并指定我的最终解决方案)吗?另外,我为什么要考虑我班级的数据表示?
【解决方案4】:

轮到我了。 纯 C++03,没有聚合。

struct B {
    B(): i_(read()), j_(read()) {}
    int i() { return i_; }
    int j() { return j_; }
private:
    int read() { int retVal; cin >> retVal; return retVal; }
    const int i_, j_;
};

如果只针对一个属性,事情可能会更简单:

struct B {
    B();
    operator int() { return i; }
private:
    const int i;
};

【讨论】:

  • 这不是我的A 类的复杂版本吗?
  • 谢谢,已编辑。此外,这个被隐式强制转换为 int。
  • 对不起,我说的是你的第一个代码。在我看来,您只是通过创建属性const 并将它们的构造委托给read() 来解决问题。但是,由于属性已经是私有的,我在这里看不到 const 的好处。
  • OP 特别不喜欢吸气剂的想法。大概@Nelxost 的类包含比这个答案中显示的两个更多的数据成员。另外:私人数据是hackable。答案是暴露私人数据或抛弃 constness 是“不要那样做!”
  • @DavidHammen #define private public 现在是我的最爱。 :D
【解决方案5】:

您使用的是 C++11 所以...为什么不使用默认初始化?

B 派生解决方案

#include <iostream>

struct B2 {
    const int i { fromFile("filename1") };
    const int j { fromFile("filename2") };
    const int k { fromFile("filename3") };

private:
    static int fromFile (const std::string &) {
        int i;
        std::cin >> i;
        return i;
    }
};

int main ()
 {
   B2 a;

   // a.i = 5; // error: i is const
   // a.j = 7; // error: j is const
   // a.k = 9; // error: k is const

   return 0;
 }

【讨论】:

  • 很抱歉,如果我的帖子中不清楚,但实际上该结构将具有多个属性。而且我看不到如何将您的解决方案扩展到多个属性。
  • @Nelxiost - “属性”是指“成员”吗?在这种情况下,是的:很清楚;但我没有看到问题。我已经修改了初始化 3 个 const 成员的答案
  • 哦,我明白了。突然,它看起来有点像马克 B 的答案。我更喜欢这种风格,带有聚合初始化,但我需要一个模板来读取不同类型的值。但是,我遇到的问题是fromFile 的实现。除了从不同文件中读取第一个值之外,我似乎无能为力。我想我可以向fromFile 添加参数,以便读取某一行或找到一个键(如果文件的格式类似于字典)。我的 B 类型没有那么严格。
  • 哦,我所说的“属性”是指数据成员。我刚刚意识到它被用于完全不相关的事情,所以我在我的问题中替换了它。
  • 我目前拥有的是对fromFile 的一次调用,它通过遵循特定规则来初始化每个数据成员。问题是,在您当前的代码中,考虑到您使用参数而不是使用 std::cin 打开文件,您只能从(不同或不同)文件中获取第一个值。
【解决方案6】:

如果我没理解的话,你需要通过调用一个函数来初始化你类的所有数据。

我想你可以将你的类包裹在另一个数据类上;例如,假设你需要一个int、一个long和一个std::string,你可以这样做

#include <iostream>

struct B3_Data {
   const int i;
   const long j;
   const std::string k;
};

struct B3 {

   using  dataType = B3_Data;

   const dataType  data = fromFile();

private:
   static dataType fromFile () {
      int i;
      long j;
      std::string k;
      std::cin >> i >> j >> k;
      return dataType {i, j, k};
    }
};

int main ()
 {
   B3 a;

   //a.data.i = 5; // error: data.i and i are const

   return 0;
 }

如果你没有太多数据,你可以把它们全部打包在一个std::tuple中;像

#include <tuple>
#include <iostream>

struct B4 {

   using  dataType = std::tuple<int, long, std::string>;

   const dataType  data { fromFile() };

private:
   static dataType fromFile () {
      int i;
      long j;
      std::string k;
      std::cin >> i >> j >> k;
      return std::make_tuple(i, j, k);
    }
};


int main ()
 {
   B4 a;

   std::cout << std::get<0>(a.data) << std::endl;  // ok: reading

   // std::get<0>(a.data) = 5;                        // error: data is const

   return 0;
 }

【讨论】:

    【解决方案7】:

    您正在寻找的是成员初始化列表返回类型优化。成员初始化器列表是初始化非静态 const 成员的地方:

    struct A{
       A():i{some_function()}{}
       const int i;
    }
    

    成员初始化器列表由构造函数声明之后和构造函数定义之前的“:”引入::i{some_function}

    somme_function 可以是任何可调用的。为了使其保持最佳状态,请启用返回类型优化。成员i 不会有返回值的副本。例如:

    int some_function(){
      int a;
      cin >> a;
      return a;
    }
    

    变量a是在调用者的上下文中构造的:变量a“引用”了成员i。不会有副本。上面的代码是最优的,它直接将你的文件写入成员i

    声明:

    auto a = A();
    

    就生成的代码而言,等同于:

    cin >> a.i;
    

    要启用此优化,请在函数体中声明返回值并按值返回。

    如果需要多个变量,解决方案是匿名联合(标准)和匿名结构(非标准):

    struct A{
      struct data_structure {
        int i;
        int j;
        };
      struct raw_data_t{
        unsigned char data[sizeof(data_structure)];
      };
      union{
        const raw_data_t raw_data;
        struct{
        const int i;
        const int j;
        };
      };
      A():raw_data{some_function()}{}
      raw_data_t some_function(){
        raw_data_t raw_data;
        auto data = new(&raw_data) data_structure();
        cin >> data->i;
        cin >> data->j;
        return raw_data;
      }
    };
    

    这不那么性感了!而且不是标准的,所以不便携。

    所以让我们等待 C++17 operator.() 重载!!

    【讨论】:

    • 谢谢,但这是我对B 所做的,除了这里,它不适用于多个数据成员。而且我知道返回值优化,但它是题外话。
    • 好的,你说得对,你需要一个匿名联合和非标准匿名结构!我以为你拒绝 B 是因为你不知道这个优化。
    【解决方案8】:

    您提到为您的类型强制const-ness 作为解决方案,但不确定如何强制客户端将您的对象构造为const。您可以使用工厂来实现此目标:

    struct MyType {
     public:
      static const MyType FromStream(std::istream &is) {
        return MyType(is);  // Most likely optimized as a move through copy elision 
      }
      static const MyType *MakeNewFromStream(std::istream &is) {
        return new MyType(is);
      }
    
      int a, b;
     private:
      MyType(std::istream &is) {
        is >> a >> b;
      }
    };
    

    并在使用中:

    const MyType mt = MyType::FromStream(std::cin);
    const MyType mt_ptr = MyType::MakeNewFromStream(std::cin);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-10-28
      • 2012-05-28
      • 2015-08-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多