我有下面的c++示例(godbolt),它在函数foo
和bar
中构造了MyStruct
的元组:
#include <iostream>
#include <tuple>
struct MyStruct {
MyStruct() = default;
MyStruct(const MyStruct&) {
std::cout << "Copy constructor called" << std::endl;
}
MyStruct(MyStruct&&) noexcept {
std::cout << "Move constructor called" << std::endl;
}
~MyStruct() = default;
MyStruct& operator=(const MyStruct&) = default;
MyStruct& operator=(MyStruct&&) noexcept = default;
};
std::tuple<MyStruct, MyStruct> foo() {
return {MyStruct{}, MyStruct{}};
}
std::tuple<MyStruct, MyStruct> bar() {
return {{}, {}};
}
int main() {
std::cout << "Foo" << std::endl;
auto [a, b] = foo();
std::cout << "Bar" << std::endl;
auto [c, d] = bar();
return 0;
}
并生成以下输出:
Foo
Move constructor called
Move constructor called
Bar
Copy constructor called
Copy constructor called
当我将这段代码放入c++ insights中时,它为foo
和bar
创建了相同的函数。所以,我的理解是foo和bar都应该移动对象,而不是bar复制它。””””有没有人知道为什么他们的行为是不同的?**
This的问题是类似的,但它是不一样的,因为我想知道为什么bar
复制的值而不是移动它。
2条答案
按热度按时间ttp71kqs1#
cppinsights.io 在这里是错误的,它不会产生与原始代码具有相同语义的代码。
return {{}, {}}
调用复制构造函数的原因是std::tuple
奇怪的构造函数和重载解析的组合。这里有两个重要的构造函数:在这两个return-语句中,都返回了prvalues,使(3)成为一个更好的匹配,因为转换序列更短(没有添加
const
)。如果可能的话,将选择这个完美的转发重载,并调用移动构造函数。但是,这对于
{{}, {}}
是不可能的,因为Utypes
包中的类型不能从{}
推断。通常,只能在可以推断类型的上下文中使用这些大括号表达式。例如:因此,
{{}, {}}
将使用第一个构造函数,这涉及到复制构造。我们可以像这样重现这个问题:此代码编译为:
0lvr5msh2#
您提供的代码正在使用
MyStruct
示例初始化元组。foo
和bar
函数之间的行为差异与元组的初始化方式有关。在
foo
函数中,使用MyStruct{}
创建MyStruct
的示例,并使用这些示例创建元组。在这里,示例是右值,所以调用了move构造函数。在
bar
函数中,您使用了brace-elision直接在元组中构造MyStruct
示例。然而,由于语言规范中的一个特殊性,移动构造函数并不一定会被使用,尽管您可能期望它会被使用。该标准允许在某些情况下执行优化(称为复制省略),以删除不必要的复制或移动操作。此优化发生在foo
中。在bar
中,元素从花括号初始化列表的相应元素复制初始化。这种差异可能是由于不同的编译器如何优化对象的创建,并且可能在所有编译器或给定编译器内的所有设置之间不一致。
使用不同的编译器设置或同一编译器的不同版本,可能会看到不同的结果。例如,您可能会发现,打开优化(
-O2
或-O3
)后,编译器能够省略bar
和foo
中的复制或移动。然而,这个例子很好地展示了C的规则有时会以微妙的方式影响性能和行为。如果需要确保不进行任何复制,那么在初始化元组时显式使用
std::move
可能是最安全的。请注意,根据C17,编译器需要在可能的情况下省略副本,但这并不意味着它们总是这样做,在某些情况下(如
bar
函数),它们可能无法做到。这是一种实施质量的问题。