表示不能将ctor-initialization-list初始化的值

时间:2018-02-16 13:08:08

标签: c++ optional unique-ptr idiomatic object-construction

我正在写一个类C,其成员foo的类型为foo_t。必须在C实例的整个生命周期内定义此成员并使其有效;但是,我没有必要的信息来在编译时构建它,即我不能使用

class C {
    foo_t foo { arg };
}

C的ctor被调用时,我也无法构建它,即我不能拥有

class C {
    foo_t foo;
    C (whatever) : foo(compute_arg(whatever)) { }
}

相反,我只能在C ctor中的某些代码运行后构建它。 编辑:这可能是因为我需要运行一些带副作用的代码(例如磁盘或网络I / O)来获取构造参数;我还需要运行代码才能初始化其他成员,所以我不能只是从初始化列表中多次调用它作为自由函数。

那么,我该如何表示foo

  • 如果foo_t可以使用一些虚拟/无效/空/空值进行默认构造,那么我可以让它发生,并且知道它永远不会在该虚拟状态下被访问。 损害:fooC的声明并不表示它始终有效
  • 如果foo_t只有一个有效状态,即在我掌握相关信息之前根本无法构建它,那么:

    • 我可以使用std::unique_ptr<foo_t>;最初它将是nullptr,然后被分配给。 损害:C()结束后没有迹象表明它永远不会为空;无用的分配。
    • 我可以使用std::optional<foo_t>;最初它将是nullopt,然后被分配给。 损害:C()结束后没有迹象表明它永远不会是空的;需要C ++ 14; “可选”一词表示拥有foo是“可选的”,而不是

我对第二种情况更感兴趣,因为在第一种情况下,关于foo_t有效性的模糊性是内置的。是否有更好的替代我提到的两个?

注意:我们无法改变foo_t

2 个答案:

答案 0 :(得分:1)

让我们考虑更具体的案例

struct C {
    foo_t foo1;
    foo_t foo2;
    C () : 
        foo1(read_from_file()),
        foo2(read_from_file()),
    { }

    static whatever_t read_from_file();
}

让我们假设不希望两次从文件中读取相同的数据。

一种可能的方法是:

struct C {
    foo_t foo1;
    foo_t foo2;

    C(): C{Create()} {}

private:
    static C Create()
    {
        return C{read_from_file()};
    }

    C(whatever_t whatever):
        foo1{whatever},
        foo2{whatever}
    {}

    static whatever_t read_from_file();
}

感谢@VittorioRomeo提出改进建议。

Wandbox

答案 1 :(得分:1)

通常,如果您可以在某个类的构造函数体中构造foo_t(没有成员初始化列表),那么您可以修改代码,以便您的类现在具有foo_t属性,它的构造函数委托构造或在其成员初始化列表中构造它。

基本上,在大多数情况下,您可以重写有问题的构造函数,以便它委托给另一个构造函数,同时为其提供必要的信息,以在成员初始化列表中构造foo_t实例(我在下面快速非正式地说明了这一点)以下“示例”https://ideone.com/ubbbb7

的评论

更一般地说,如果元组结构由于某种原因而成为问题,则以下转换(通常)将起作用。这无疑是有点长(而且丑陋),但请记住这是为了一般性的缘故,并且可能在实践中简化了事情。

假设我们有一个构造函数,我们构造一个foo_t,为了简单起见,我们将进一步假设它具有以下形式:

C::C(T1 arg_1, T2 arg_2) {
    side_effects(arg_1, arg_2);
    TL1 local(arg_1, arg_2);
    second_side_effects(arg_1, arg_2, local);
    foo_t f(arg_1, arg_2, local); // the actual construction
    final_side_effects(arg_1, arg_2, local, f);
}

函数调用可能会改变参数。 我们可以委托一次消除构造函数体中local_1的声明,然后再次删除对second_side_effects(arg_1, arg_2, local)的调用。

C::C(T1 arg_1, T2 arg_2)
: C::C(arg_1, arg_2
      ,([](T1& a, T2& b){
          side_effects(a, b);
        }(arg_1, arg_2), TL1(a, b))) {}

C::C(T1& arg_1, T2& arg_2, TL1&& local)
: C::C(arg_1, arg_2
      ,[](T1& a, T2& b, TL1& c) -> TL1& {
          second_side_effects(a, b, c);
          return c;
      }(arg_1, arg_2, local)) {}

C::C(T1& arg_1, T2& arg_2, TL1& local) {
    foo_t f(arg_1, arg_2, local); // the actual construction
    final_side_effects(arg_1, arg_2, local, f);
}

live example

显然,f可以成为C的实际成员,并在最后一个构造函数的成员初始化列表中构建。

可以推广任意数量的局部变量(和参数)。但是我假设我们的初始构造函数没有任何成员初始化列表。如果有的话,我们可能需要:

  • 在变异之前复制一些初始arg_i并在构造函数链中传递副本,以便它们最终可用于构建成员初始化列表中的其他成员
  • 预构建成员的实例并沿构造函数链传递它们,以便它们最终可用于移动构造成员初始化列表中的实际成员

如果由于某种原因,成员的构造函数会产生副作用,则必须选择后者。

然而,有一种情况是这一切都崩溃了。让我们考虑以下情况:

#include <memory>

struct state_t; // non copyable, non movable

// irreversible function that mutates an instance of state_t
state_t& next_state(state_t&);

struct foo_t {
    foo_t() = delete;
    foo_t(const foo_t&) = delete;
    foo_t(const state_t&);
};

// definitions are elsewhere

class C {
public:
    struct x_first_tag {};
    struct y_first_tag {};

    // this constructor prevents us from reordering x and y
    C(state_t& s, x_first_tag = {})
    : x(new foo_t(s))
    , y(next_state(s)) {}

    // if x and y were both constructed in the member initializer list
    // x would be constructed before y
    // but the construction of y requires the original s which will
    // be definitively lost when we're ready to construct x !
    C(state_t& s, y_first_tag = {})
    : x(nullptr)
    , y(s) {
        next_state(s);
        x.reset(new foo_t(s));
    }

private:
    std::unique_ptr<foo_t> x; // can we make that a foo_t ?
    foo_t y;
};

在那种情况下,我承认不知道如何改写这门课程,但我认为这种情况很少见,并不重要。