c++ 通过返回构造元组时的移动规则

brtdzjyr  于 2023-06-25  发布在  其他
关注(0)|答案(2)|浏览(221)

我有下面的c++示例(godbolt),它在函数foobar中构造了MyStruct的元组:

  1. #include <iostream>
  2. #include <tuple>
  3. struct MyStruct {
  4. MyStruct() = default;
  5. MyStruct(const MyStruct&) {
  6. std::cout << "Copy constructor called" << std::endl;
  7. }
  8. MyStruct(MyStruct&&) noexcept {
  9. std::cout << "Move constructor called" << std::endl;
  10. }
  11. ~MyStruct() = default;
  12. MyStruct& operator=(const MyStruct&) = default;
  13. MyStruct& operator=(MyStruct&&) noexcept = default;
  14. };
  15. std::tuple<MyStruct, MyStruct> foo() {
  16. return {MyStruct{}, MyStruct{}};
  17. }
  18. std::tuple<MyStruct, MyStruct> bar() {
  19. return {{}, {}};
  20. }
  21. int main() {
  22. std::cout << "Foo" << std::endl;
  23. auto [a, b] = foo();
  24. std::cout << "Bar" << std::endl;
  25. auto [c, d] = bar();
  26. return 0;
  27. }

并生成以下输出:

  1. Foo
  2. Move constructor called
  3. Move constructor called
  4. Bar
  5. Copy constructor called
  6. Copy constructor called

当我将这段代码放入c++ insights中时,它为foobar创建了相同的函数。所以,我的理解是foo和bar都应该移动对象,而不是bar复制它。””””有没有人知道为什么他们的行为是不同的?**
This的问题是类似的,但它是不一样的,因为我想知道为什么bar复制的值而不是移动它。

ttp71kqs

ttp71kqs1#

cppinsights.io 在这里是错误的,它不会产生与原始代码具有相同语义的代码。return {{}, {}}调用复制构造函数的原因是std::tuple奇怪的构造函数和重载解析的组合。这里有两个重要的构造函数:

  1. tuple( const Types&... args ); // (2)
  2. template< class... UTypes >
  3. tuple( UTypes&&... args ); // (3)

在这两个return-语句中,都返回了prvalues,使(3)成为一个更好的匹配,因为转换序列更短(没有添加const)。如果可能的话,将选择这个完美的转发重载,并调用移动构造函数。
但是,这对于{{}, {}}是不可能的,因为Utypes包中的类型不能从{}推断。通常,只能在可以推断类型的上下文中使用这些大括号表达式。例如:

  1. void take_int(int);
  2. void take_any(auto);
  3. int main() {
  4. take_int({}); // OK, value initialization of an int
  5. take_any({}); // ill-formed, cannot infer type from {}
  6. }

因此,{{}, {}}将使用第一个构造函数,这涉及到复制构造。我们可以像这样重现这个问题:

  1. template <typename ...Ts>
  2. struct tuple {
  3. tuple(const Ts&...);
  4. template <typename ...Us>
  5. tuple(Us&&...);
  6. };
  7. struct MyStruct {
  8. MyStruct() = default;
  9. MyStruct(const MyStruct&);
  10. MyStruct(MyStruct&&) noexcept;
  11. };
  12. tuple<MyStruct, MyStruct> foo() {
  13. return {MyStruct{}, MyStruct{}};
  14. }
  15. tuple<MyStruct, MyStruct> bar() {
  16. return {{}, {}};
  17. }

此代码编译为:

  1. foo():
  2. # ...
  3. call tuple<MyStruct, MyStruct>::tuple<MyStruct, MyStruct>(MyStruct&&, MyStruct&&)@PLT
  4. # ...
  5. ret
  6. bar():
  7. # ...
  8. call tuple<MyStruct, MyStruct>::tuple(MyStruct const&, MyStruct const&)@PLT
  9. # ...
  10. ret
展开查看全部
0lvr5msh

0lvr5msh2#

您提供的代码正在使用MyStruct示例初始化元组。foobar函数之间的行为差异与元组的初始化方式有关。
foo函数中,使用MyStruct{}创建MyStruct的示例,并使用这些示例创建元组。在这里,示例是右值,所以调用了move构造函数。
bar函数中,您使用了brace-elision直接在元组中构造MyStruct示例。然而,由于语言规范中的一个特殊性,移动构造函数并不一定会被使用,尽管您可能期望它会被使用。该标准允许在某些情况下执行优化(称为复制省略),以删除不必要的复制或移动操作。此优化发生在foo中。在bar中,元素从花括号初始化列表的相应元素复制初始化。
这种差异可能是由于不同的编译器如何优化对象的创建,并且可能在所有编译器或给定编译器内的所有设置之间不一致。
使用不同的编译器设置或同一编译器的不同版本,可能会看到不同的结果。例如,您可能会发现,打开优化(-O2-O3)后,编译器能够省略barfoo中的复制或移动。
然而,这个例子很好地展示了C的规则有时会以微妙的方式影响性能和行为。如果需要确保不进行任何复制,那么在初始化元组时显式使用std::move可能是最安全的。
请注意,根据C
17,编译器需要在可能的情况下省略副本,但这并不意味着它们总是这样做,在某些情况下(如bar函数),它们可能无法做到。这是一种实施质量的问题。

相关问题