为什么获取 std::tuple return 右值引用而不是值的助手
Why does get helper of std::tuple return rvalue reference instead of value
如果您查看 get
,std::tuple
的辅助函数,您会注意到以下重载:
template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >&&
get( tuple<Types...>&& t );
换句话说,当输入元组本身是一个右值引用时,它return是一个右值引用。为什么不按值return,在函数体中调用move
?我的论点如下:get 的 return 要么绑定到一个引用,要么绑定到一个值(我想它可以绑定到任何东西,但这不应该是一个常见的用例)。如果它绑定到一个值,那么移动构造无论如何都会发生。因此,按值 return 不会有任何损失。如果您绑定到一个引用,那么 returning 一个右值引用实际上是不安全的。举个例子:
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;
}
当运行时,这个程序打印:
Constructed at : 0x7ffc0e12cdc0
Destructed at : 0x7ffc0e12cdc0
0
换句话说,x 立即成为悬空引用。而如果你只是像这样写 foo:
struct foo {
Hello m_hello;
Hello get() && { return std::move(m_hello); }
};
不会出现这个问题。此外,如果你像这样使用 foo:
Hello x(foo().get());
无论是 return 按值还是右值引用,似乎都没有任何额外开销。我已经测试过这样的代码,它似乎会非常一致地只执行一次移动构造。例如。如果我添加一个成员:
Hello(Hello && ) { std::cerr << "Moved" << std::endl; }
并且我按上面的方式构造 x,无论我是 return 按值还是按右值引用,我的程序只打印 "Moved" 一次。
我失踪是有充分理由的,还是疏忽?
注意:这里有一个很好的相关问题:Return value or rvalue reference?。似乎说 return 在这种情况下通常更可取,但它出现在 STL 中的事实让我很好奇 STL 是否忽略了这个推理,或者他们是否有自己的特殊原因可能一般不适用。
编辑:有人建议此问题与 Is there any case where a return of a RValue Reference (&&) is useful? 重复。不是这种情况;这个答案建议 return 通过右值引用作为一种消除数据成员复制的方法。正如我在上面详细讨论的那样,如果您首先调用 move
,则无论您是 return 按值还是按右值引用,复制都将被省略。
关于如何使用它来创建悬挂引用的示例非常有趣,但从示例中吸取正确的教训很重要。
考虑一个更简单的例子,它在任何地方都没有任何 &&
:
const int &x = vector<int>(1) .front();
.front()
returns 一个 &
对新构造向量的第一个元素的引用。 vector 当然会立即被销毁,你会留下一个悬空的引用。
要吸取的教训是,使用 const
-reference 通常不会延长生命周期。它延长了 非引用 的生命周期。如果=
右边是参考,那你就得自己承担一辈子的责任了。
情况一直如此,因此 tuple::get
做任何不同的事情都没有意义。 tuple::get
允许 return 引用,就像 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;
}
这将允许优化的额外重载:
vector<int>&& concat(vector<int>&& l, const vector<int> &r) {
l.insert(l.end(), r.cbegin(), r.cend());
return l;
}
此优化将构造的数量保持在最低限度
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);
最后一行有四次调用 concat
,只有两个结构:起始值 (vector<int>()
) 和进入 c
的移动结构。您可以在那里对 concat
进行 100 次嵌套调用,而无需任何额外的构造。
因此,returning by &&
可以更快。因为,是的,移动比复制快,但如果你能避免两者,它甚至更快。
总而言之,这是为了速度。考虑在元组内元组上使用 get
的嵌套系列。此外,它允许它使用既没有复制也没有移动构造函数的类型。
而且这不会引入任何关于使用寿命的新风险。 vector<int>().front()
"problem" 不是新的。
如果您查看 get
,std::tuple
的辅助函数,您会注意到以下重载:
template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >&&
get( tuple<Types...>&& t );
换句话说,当输入元组本身是一个右值引用时,它return是一个右值引用。为什么不按值return,在函数体中调用move
?我的论点如下:get 的 return 要么绑定到一个引用,要么绑定到一个值(我想它可以绑定到任何东西,但这不应该是一个常见的用例)。如果它绑定到一个值,那么移动构造无论如何都会发生。因此,按值 return 不会有任何损失。如果您绑定到一个引用,那么 returning 一个右值引用实际上是不安全的。举个例子:
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;
}
当运行时,这个程序打印:
Constructed at : 0x7ffc0e12cdc0
Destructed at : 0x7ffc0e12cdc0
0
换句话说,x 立即成为悬空引用。而如果你只是像这样写 foo:
struct foo {
Hello m_hello;
Hello get() && { return std::move(m_hello); }
};
不会出现这个问题。此外,如果你像这样使用 foo:
Hello x(foo().get());
无论是 return 按值还是右值引用,似乎都没有任何额外开销。我已经测试过这样的代码,它似乎会非常一致地只执行一次移动构造。例如。如果我添加一个成员:
Hello(Hello && ) { std::cerr << "Moved" << std::endl; }
并且我按上面的方式构造 x,无论我是 return 按值还是按右值引用,我的程序只打印 "Moved" 一次。
我失踪是有充分理由的,还是疏忽?
注意:这里有一个很好的相关问题:Return value or rvalue reference?。似乎说 return 在这种情况下通常更可取,但它出现在 STL 中的事实让我很好奇 STL 是否忽略了这个推理,或者他们是否有自己的特殊原因可能一般不适用。
编辑:有人建议此问题与 Is there any case where a return of a RValue Reference (&&) is useful? 重复。不是这种情况;这个答案建议 return 通过右值引用作为一种消除数据成员复制的方法。正如我在上面详细讨论的那样,如果您首先调用 move
,则无论您是 return 按值还是按右值引用,复制都将被省略。
关于如何使用它来创建悬挂引用的示例非常有趣,但从示例中吸取正确的教训很重要。
考虑一个更简单的例子,它在任何地方都没有任何 &&
:
const int &x = vector<int>(1) .front();
.front()
returns 一个 &
对新构造向量的第一个元素的引用。 vector 当然会立即被销毁,你会留下一个悬空的引用。
要吸取的教训是,使用 const
-reference 通常不会延长生命周期。它延长了 非引用 的生命周期。如果=
右边是参考,那你就得自己承担一辈子的责任了。
情况一直如此,因此 tuple::get
做任何不同的事情都没有意义。 tuple::get
允许 return 引用,就像 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;
}
这将允许优化的额外重载:
vector<int>&& concat(vector<int>&& l, const vector<int> &r) {
l.insert(l.end(), r.cbegin(), r.cend());
return l;
}
此优化将构造的数量保持在最低限度
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);
最后一行有四次调用 concat
,只有两个结构:起始值 (vector<int>()
) 和进入 c
的移动结构。您可以在那里对 concat
进行 100 次嵌套调用,而无需任何额外的构造。
因此,returning by &&
可以更快。因为,是的,移动比复制快,但如果你能避免两者,它甚至更快。
总而言之,这是为了速度。考虑在元组内元组上使用 get
的嵌套系列。此外,它允许它使用既没有复制也没有移动构造函数的类型。
而且这不会引入任何关于使用寿命的新风险。 vector<int>().front()
"problem" 不是新的。