复制省略与结构化绑定一起使用是否有效?

26

强制拷贝省略是否适用于通过结构化绑定进行的分解?它适用于以下哪些情况?

// one
auto [one, two] = std::array<SomeClass>{SomeClass{1}, SomeClass{2}};

// two
auto [one, two] = std::make_tuple(SomeClass{1}, SomeClass{2});

// three
struct Something { SomeClass one, two; };
auto [one, two] = Something{};    

我怀疑只有第三种情况允许复制省略,因为前两种情况将通过std::get<>std::tuple_size<>进行“分解”,当参数为rvalue时std::get<>返回xvalue。

从标准中引用一句话也不错!


是的,当你考虑结构化绑定实际上是什么解糖时,就很容易理解为什么了。 ;-] - ildjarn
1
我的意思是,“是的,拷贝省略可以与结构化绑定一起使用” - onethree 将会保证进行拷贝省略,但 two 不会。 - ildjarn
但是这些绑定不是标准的吗?数组需要额外的参数。 - Swift - Friday Pie
@ildjarn,但是只允许通过 get<>tuple_size 函数/方法/特性来访问?按照标准的阅读,右侧的元素必须是数组或者有所有公共成员的类。能否以这种方式递归使用结构化绑定呢? - Curious
@Curious:再次强调,重要的是要理解结构化绑定实际上是什么。get<>tuple_size都是无关紧要的。 - ildjarn
显示剩余4条评论
2个回答

22
强制复制省略适用于通过结构化绑定进行解构吗?它适用于以下哪些情况?是的,所有这些情况都适用。结构化绑定的目的是为您提供对绑定类型的解构元素的命名引用。
auto [one, two] = expr;

只是语法糖而已:
auto __tmp = expr;
some_type<0,E>& one = some_getter<0>(__tmp);
some_type<1,E>& two = some_getter<1>(__tmp);

在解构类型(数组、类似元组的类型或者具有所有公共非静态数据成员的类型)时,some_typesome_getter取决于类型的种类。

auto __tmp = expr这一行中,强制执行复制省略,其他行都不涉及复制。


在评论中有一些关于示例的混淆,所以让我详细解释一下在这个示例中发生了什么。
auto [one, two] = std::make_tuple(Something{}, Something{});

那个展开为
auto __tmp = std::make_tuple(Something{}, Something{}); // note that it is from
// std::make_tuple() itself that we get the two default constructor calls as well
// as the two copies.
using __E = std::remove_reference_t<decltype(__tmp)>; // std::tuple<Something, Something>

然后,由于__E非数组类型类似于元组}所以我们通过对get的不限定调用引入变量,在__E的关联命名空间中查找。初始化程序将是一个{{link4:xvalue,类型将是rvalue引用

std::tuple_element_t<0, __E>&& one = get<0>(std::move(__tmp));
std::tuple_element_t<1, __E>&& two = get<1>(std::move(__tmp));

请注意,尽管onetwo都是对__tmp的右值引用,但decltype(one)decltype(two)将会都返回Something而不是Something&&

2
@Curious:结构化绑定并不具备这样的功能。它根本不会创建任何类。 - Nicol Bolas
1
挑剔一点:双下划线命名(__tmp)是被保留的,不要在示例中使用这样的名称。 - Jesper Juhl
2
@Curious make_tuple(以及一般的tuple)在创建元组时需要实例化临时变量以绑定到make_tuple(或tuple的构造函数)的引用参数。这与结构化绑定无关。 - T.C.
9
我在示例中使用双下划线命名以说明语言引入的名称,就像标准使用__range__begin__end来定义基于范围的for语句的含义一样。请注意,此处的翻译并未添加解释性内容,且尽可能保留原文意思和风格。 - Barry
7
这段话意在说明编译器如何对结构化绑定声明进行解糖。而且编译器最好为引入的变量使用保留名称! - T.C.
显示剩余18条评论

4

有趣的问题:

#include <iostream>
#include <array>
#include <tuple>
#include <typeinfo>
using std::cout;
using std::endl;

struct SomeClass
{
    int baz;

    SomeClass(int _b): baz(_b) {
        cout << __PRETTY_FUNCTION__ << " = " << baz << endl;
    }
    SomeClass(SomeClass&&) {
        cout << __PRETTY_FUNCTION__ << endl;
    }
    SomeClass(const SomeClass&) {
        cout << __PRETTY_FUNCTION__ << endl;
    }
};

template<typename T> void tell(T&& a)
{
    cout << "Tell: " << __PRETTY_FUNCTION__ << " = " << a.baz << endl;
}

int main()
{
     // one
     cout << "= 1 =" << endl;
     auto [one, two] = std::array<SomeClass,2>{SomeClass{1}, SomeClass{2}};
     cout << "===" << endl;
     tell(one); tell(two);
     // two
     cout << endl << "= 2 =" << endl;
     auto [one2, two2] = std::make_tuple(SomeClass{1}, SomeClass{2});
     cout << "===" << endl;
     tell(one2); tell(two2);
     // three
     cout << endl << "= 3 =" << endl;
     struct Something { SomeClass one{1}, two{2}; };     
     auto [one3, two3] = Something{}; 
     cout << "===" << endl;
     tell(one3); tell(two3);

    return 0;
}

产生输出:

= 1 =
SomeClass::SomeClass(int) = 1
SomeClass::SomeClass(int) = 2
===
Tell: void tell(T&&) [with T = SomeClass&] = 1
Tell: void tell(T&&) [with T = SomeClass&] = 2

= 2 =
SomeClass::SomeClass(int) = 2
SomeClass::SomeClass(int) = 1
SomeClass::SomeClass(SomeClass&&)
SomeClass::SomeClass(SomeClass&&)
===
Tell: void tell(T&&) [with T = SomeClass&] = 0
Tell: void tell(T&&) [with T = SomeClass&] = 4199261

= 3 =
SomeClass::SomeClass(int) = 1
SomeClass::SomeClass(int) = 2
===
Tell: void tell(T&&) [with T = SomeClass&] = 1
Tell: void tell(T&&) [with T = SomeClass&] = 2

第二种情况使用复制或移动(如果可用)构造函数。 值未初始化,因为我故意没有在构造函数中这样做。

绑定有三种协议:

  • 绑定到数组
  • 绑定到类似元组的类型
  • 绑定到公共数据成员

在第二种情况下(抱歉,我没有访问C++17 pdf,所以参考cppreference):

每个标识符都成为一个变量,其类型为“对std::tuple_element<i,E> ::type的引用”,如果相应的初始化程序是左值,则为左值引用,否则为右值引用。第i个标识符的初始化程序为

  • e.get<i>(),如果在类成员访问查找中,在E的范围内查找标识符get可以找到至少一个声明(无论是什么类型)
  • 否则,使用get<i>(e),其中只通过参数相关的查找来查找get,忽略非ADL查找

例子的第一阶段和第二阶段实际上是绑定到类似元组的类型。但是...在第二阶段中,我们用什么来初始化?一个构造元组的模板函数:

 std::make_tuple(SomeClass{1}, SomeClass{2});

这将实际上复制或移动值。进一步的复制省略可能会发生,但是

 auto t = std::make_tuple(SomeClass{1}, SomeClass{2});
 auto [one2, two2] = t;

将会产生以下输出:

SomeClass::SomeClass(int) = 2
SomeClass::SomeClass(int) = 1
SomeClass::SomeClass(SomeClass&&)      //make_tuple
SomeClass::SomeClass(SomeClass&&)
SomeClass::SomeClass(const SomeClass&) //assignment 
SomeClass::SomeClass(const SomeClass&)

虽然正确的去糖化结构绑定看起来像这样:

 auto t = std::make_tuple(SomeClass{1}, SomeClass{2});
 auto& one2 = std::get<0>(t);
 auto& two2 = std::get<1>(t);

并且输出与原始内容匹配:
SomeClass::SomeClass(int) = 2
SomeClass::SomeClass(int) = 1
SomeClass::SomeClass(SomeClass&&)
SomeClass::SomeClass(SomeClass&&)
===

所以,发生的复制或移动操作是从构造我们的tuple开始的。如果我们使用通用引用构造元组,则可以避免这种情况,然后解糖化两者。
 auto t = std::tuple<SomeClass&&, SomeClass&&>(SomeClass{1}, SomeClass{2});
 auto& one2 = std::get<0>(t);
 auto& two2 = std::get<1>(t);

以及结构化绑定

 auto [one2, two2] = std::tuple<SomeClass&&, SomeClass&&>(SomeClass{1}, SomeClass{2});

会导致复制省略。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接