英文:
Rules for move when constructing a tuple by return
问题
我有以下的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
会复制值而不是移动它。
英文:
I have the following c++ example (godbolt) that constructs a tuple of MyStruct
in the functions foo
and bar
:
#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;
}
And produces the following output:
Foo
Move constructor called
Move constructor called
Bar
Copy constructor called
Copy constructor called
When I put this code in c++ insights, it creates the same function for both foo
and bar
. So, my understanding is that both foo and bar should move the object instead of bar copying it. Does anyone know why the behaviour is different?
This question is similar but it's not the same as I'm wondering why bar
copies the value instead of moving it.
答案1
得分: 3
cppinsights.io在这里是错误的,它不会生成与您的原始代码具有相同语义的代码。
return {{}, {}}
调用复制构造函数的原因是 std::tuple
的奇怪构造函数和重载分辨率的组合。这里有两个重要的构造函数:
tuple(const Types&... args); // (2)
template< class... UTypes >
tuple(UTypes&&... args); // (3)
在您的两个返回语句中,您都返回 prvalues,这使得 (3) 更匹配,因为转换序列更短(没有添加 const
)。
如果可能的话,这个完美转发的重载将被选择,并调用移动构造函数。
然而,对于 {{}, {}}
来说,这是不可能的,因为Utypes
包中的类型无法从 {}
推断出来。一般来说,您只能在类型可以被推断的上下文中使用这些大括号表达式。
例如:
void take_int(int);
void take_any(auto);
int main() {
take_int({}); // OK,int的值初始化
take_any({}); // 非法,无法从 {} 推断类型
}
因此,{{}, {}}
将使用第一个构造函数,这涉及到复制构造。
我们可以像这样重现这个问题:
template <typename ...Ts>
struct tuple {
tuple(const Ts&...);
template <typename ...Us>
tuple(Us&&...);
};
struct MyStruct {
MyStruct() = default;
MyStruct(const MyStruct&);
MyStruct(MyStruct&&) noexcept;
};
tuple<MyStruct, MyStruct> foo() {
return {MyStruct{}, MyStruct{}};
}
tuple<MyStruct, MyStruct> bar() {
return {{}, {}};
}
这段代码编译为:
foo():
# ...
call tuple<MyStruct, MyStruct>::tuple<MyStruct, MyStruct>(MyStruct&&, MyStruct&&)@PLT
# ...
ret
bar():
# ...
call tuple<MyStruct, MyStruct>::tuple(MyStruct const&, MyStruct const&)@PLT
# ...
ret
英文:
cppinsights.io is wrong here, and doesn't produce code that has the same semantics as your original.
The reason why return {{}, {}}
calls the copy constructor is a combination of std::tuple
's weird constructors, and overload resolution. There are two important constructors here:
tuple( const Types&... args ); // (2)
template< class... UTypes >
tuple( UTypes&&... args ); // (3)
In both of your return-statements, you are returning prvalues, making (3) a better match because the conversion sequence is shorter (no const
added).
If possible, this perfect forwarding overload is going to be chosen, and the move constructor is called.
However, this is not possible for {{}, {}}
, because the types in the Utypes
pack cannot be inferred from {}
. In general, you can only use these brace expressions in a context where the type can be inferred.
For example:
void take_int(int);
void take_any(auto);
int main() {
take_int({}); // OK, value initialization of an int
take_any({}); // ill-formed, cannot infer type from {}
}
As a consequence, {{}, {}}
will use the first constructor, which involves copy construction.
We can reproduce this issue like this:
template <typename ...Ts>
struct tuple {
tuple(const Ts&...);
template <typename ...Us>
tuple(Us&&...);
};
struct MyStruct {
MyStruct() = default;
MyStruct(const MyStruct&);
MyStruct(MyStruct&&) noexcept;
};
tuple<MyStruct, MyStruct> foo() {
return {MyStruct{}, MyStruct{}};
}
tuple<MyStruct, MyStruct> bar() {
return {{}, {}};
}
This code compiles to:
foo():
# ...
call tuple<MyStruct, MyStruct>::tuple<MyStruct, MyStruct>(MyStruct&&, MyStruct&&)@PLT
# ...
ret
bar():
# ...
call tuple<MyStruct, MyStruct>::tuple(MyStruct const&, MyStruct const&)@PLT
# ...
ret
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论