std::tuple 具有可移动构造元素的默认构造函数

std::tuple default constructor with move-constructable element

我正在尝试 return 一个 std::tuple 包含一个不可复制构造类型的元素。这似乎阻止我使用默认的 class 构造函数来构造元组。例如,对于 return 一个包含 Foo 的元组,必须创建一个 foo 实例并且 std::moved:

class Foo {
  public:
    Foo(const Foo&) = delete;
    Foo(Foo&&) = default;
    int x;
};

tuple<int, Foo> MakeFoo() {
    Foo foo{37};
//  return {42, {37}}; // error: could not convert ‘{42, {37}}’ from ‘’ to ‘std::tuple’
    return {42, std::move(foo)};
}

另一方面,如果 class 被定义为具有复制构造函数,则元组的构造工作正常:

class Bar {
  public:
    Bar(const Bar&) = default;
    int x;
};

tuple<int, Bar> MakeBar() {
    return {42, {37}}; // compiles ok
}

有没有办法将 MakeBar 语法与 Foo class 一起使用?

不完全是,但您(至少)有两个选择。

您可以拼出 Foo:

tuple<int, Foo> MakeFoo() {
    return {42, Foo{37}}
}

或者您可以向 Foo 添加一个构造函数来替换您现在正在执行的聚合初始化:

class Foo {
  public:
    Foo(const Foo&) = delete;
    Foo(Foo&&) = default;
    Foo(int x) : x(x) {}      // <--
    int x;
};

tuple<int, Foo> MakeFoo() {
    return {42, 37};
}

但是为什么要写return {42, 37}而不是return {42, {37}}呢?为什么需要添加一个构造函数来完成这项工作?

构造一个至少有一个只移动类型的元组意味着我们不能使用直接构造函数

tuple<Types...>::tuple(const Types &...)

相反,我们必须使用模板转换构造函数

template<class... UTypes>
tuple<Types...>::tuple(UTypes &&...)

所以会推导出两个参数的类型。但是,{37} 是一个初始化列表,在这种情况下,它使第二个函数参数成为 非推导上下文 (请参阅 [temp.deduct.type]/5.6)。因此模板参数推导失败,并且无法调用构造函数。所以,我们必须写 return {42, 37} 来让推导成功。

此外,当参数类型可转换到相应的元组元素类型时,此模板化转换构造函数仅参与重载决策。并且可转换性不考虑聚合初始化,因此 int 不能转换为原始 Foo。但是如果我们添加转换构造函数 Foo::Foo(int)int 现在可以转换为 Foo,并且可以调用 tuple 构造函数。