Nir*_*man 5 c++ stl move-semantics c++11
如果你看一下get辅助函数std::tuple,你会注意到以下的重载:
template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >&&
get( tuple<Types...>&& t );
Run Code Online (Sandbox Code Playgroud)
换句话说,当输入元组本身是右值引用时,它返回一个右值引用.为什么不按值返回,调用move函数体?我的论点如下:get的返回将被绑定到引用,或者绑定到一个值(它可能被绑定到我想的任何东西,但这不应该是一个常见的用例).如果它与一个值绑定,那么移动构造无论如何都会发生.所以你不会因为价值回归而失去一切.如果绑定到引用,则返回右值引用实际上可能不安全.举个例子:
struct Hello {
Hello() {
std::cerr << "Constructed at : " << this << std::endl;
}
~Hello() {
std::cerr << "Destructed at : " << this << std::endl;
}
double m_double;
};
struct foo {
Hello m_hello;
Hello && get() && { return std::move(m_hello); }
};
int main() {
const Hello & x = foo().get();
std::cerr << x.m_double;
}
Run Code Online (Sandbox Code Playgroud)
运行时,该程序打印:
Constructed at : 0x7ffc0e12cdc0
Destructed at : 0x7ffc0e12cdc0
0
Run Code Online (Sandbox Code Playgroud)
换句话说,x立即成为悬空参考.如果你只是写这样的foo:
struct foo {
Hello m_hello;
Hello get() && { return std::move(m_hello); }
};
Run Code Online (Sandbox Code Playgroud)
这个问题不会发生.此外,如果你然后像这样使用foo:
Hello x(foo().get());
Run Code Online (Sandbox Code Playgroud)
无论是按值还是右值引用返回,似乎都没有任何额外的开销.我已经测试了这样的代码,似乎它将始终只执行一次移动构造.例如,如果我添加一个成员:
Hello(Hello && ) { std::cerr << "Moved" << std::endl; }
Run Code Online (Sandbox Code Playgroud)
我按上面的方式构造x,我的程序只打印"Moved"一次,无论我是按值还是右值引用返回.
我错过了一个很好的理由,还是这是一个疏忽?
注意:这里有一个很好的相关问题:返回值或右值参考?.似乎在这种情况下,价值回报通常更可取,但它在STL中出现的事实让我很好奇STL是否忽略了这种推理,或者他们是否有自己的特殊原因可能不适用通常.
编辑:有人建议这个问题是重复的是否有任何返回RValue参考(&&)有用的情况?.不是这种情况; 这个答案建议通过右值引用返回作为忽略数据成员复制的方法.正如我在上面详细讨论的那样,如果您move先调用,无论您是按值返还还是右值引用,都将省略复制.
您关于如何使用它来创建悬空引用的示例非常有趣,但从示例中学习正确的教训很重要。
考虑一个更简单的例子,它没有任何&&地方:
const int &x = vector<int>(1) .front();
Run Code Online (Sandbox Code Playgroud)
.front()返回&对新构造的向量的第一个元素的引用。当然,向量会立即被破坏,并且留下一个悬空的引用。
要吸取的教训是,使用const- 引用通常不会延长生命周期。它延长了非引用的生命周期。如果右侧=是参考,那么你就得自己承担一生的责任。
情况一直如此,所以tuple::get做任何不同的事情是没有意义的。允许像往常tuple::get一样返回引用。vector::front
您谈论移动和复制构造函数以及速度。最快的解决方案是不使用任何构造函数。想象一个连接两个向量的函数:
vector<int> concat(const vector<int> &l_, const vector<int> &r) {
vector<int> l(l_);
l.insert(l.end(), r.cbegin(), r.cend());
return l;
}
Run Code Online (Sandbox Code Playgroud)
这将允许优化的额外过载:
vector<int>&& concat(vector<int>&& l, const vector<int> &r) {
l.insert(l.end(), r.cbegin(), r.cend());
return l;
}
Run Code Online (Sandbox Code Playgroud)
这种优化将构造数量保持在最低限度
vector<int> a{1,2,3};
vector<int> b{3,4,5};
vector<int> c = concat(
concat(
concat(
concat(vector<int>(), a)
, b)
, a
, b);
Run Code Online (Sandbox Code Playgroud)
最后一行有四次调用concat,只有两个构造:起始值 ( vector<int>()) 和移动构造到c。您可以在那里进行 100 个嵌套调用concat,而无需任何额外的构造。
所以,返回&&可以更快。因为,是的,移动比复制更快,但如果你能避免两者,它甚至会更快。
总而言之,这样做是为了速度。get考虑在元组内的元组内使用嵌套系列。此外,它还允许它与既没有复制也没有移动构造函数的类型一起使用。
这不会带来任何关于生命周期的新风险。这个vector<int>().front()“问题”并不是一个新问题。