【发布时间】:2021-09-08 21:17:49
【问题描述】:
我想返回一个包含 std::vector 或 std::unordered_map 等类型的元组,其中对象可能足够大,以至于我关心不复制。当返回的对象包装在元组中时,我不确定复制省略/返回值优化将如何工作。为此,我在下面编写了一些测试代码,并对其部分输出感到困惑:
#include <tuple>
#include <iostream>
struct A {
A() {}
A(const A& a) {
std::cout << "copy constructor\n";
}
A(A&& a) noexcept {
std::cout << "move constructor\n";
}
~A() {
std::cout << "destructor\n";
}
};
struct B {
};
std::tuple<A, B> foo() {
A a;
B b;
return { a, b };
}
std::tuple<A, B> bar() {
A a;
B b;
return { std::move(a), std::move(b) };
}
std::tuple<A, B> quux() {
A a;
B b;
return std::move(std::tuple<A, B>{ std::move(a), std::move(b) });
}
std::tuple<A, B> mumble() {
A a;
B b;
return std::move(std::tuple<A, B>{ a, b });
}
int main()
{
std::cout << "calling foo...\n\n";
auto [a1, b1] = foo();
std::cout << "\n";
std::cout << "calling bar...\n\n";
auto [a2, b2] = bar();
std::cout << "\n";
std::cout << "calling quux...\n\n";
auto [a3, b3] = quux();
std::cout << "\n";
std::cout << "calling mumble...\n\n";
auto [a4, b4] = mumble();
std::cout << "\n";
std::cout << "cleaning up main()\n";
return 0;
}
当我运行上述(在 VS2019 上)时,我得到以下输出:
calling foo...
copy constructor
destructor
calling bar...
move constructor
destructor
calling quux...
move constructor
move constructor
destructor
destructor
calling mumble...
copy constructor
move constructor
destructor
destructor
cleaning up main()
destructor
destructor
destructor
destructor
所以从上面看起来bar() 最好是return { std::move(a), std::move(b) }。我的主要问题是为什么foo() 最终会复制? RVO 应该避免复制元组,但编译器不应该足够聪明以不复制 A 结构吗?元组构造函数可能是一个移动构造函数,因为它在从函数返回的表达式中触发,即因为 struct a 即将不存在。
我也不太明白quux() 发生了什么。我不认为额外的std::move() 调用是必要的,但我不明白为什么它最终会导致实际发生额外的动作,即我希望它具有与bar() 相同的输出.
【问题讨论】:
-
在
foo中,a需要被复制到临时的tuple中,这个临时的tuple会返回给调用者。退货单上的副本将被省略。 -
该标准描述了在给定情况下调用哪个构造函数,这意味着编译器在这里不能太聪明。至于
quux:首先您对创建的元组对象进行移动信息,然后通过将元组传递给std::move,您确保使用元组的移动构造函数来创建实际结果;这会调用A的移动构造函数;通过将元组传递给std::move,您可以防止可能发生的复制省略... -
还要注意,虽然对象很大,但它们的移动构造函数很快,因为没有任何内容被复制。对数据的引用从源传输到目标。
-
相关:When should std::move be used on a function return value? (tl;dr: never)
标签: c++ tuples move-semantics