2015-07-11 53 views
5

帮手如果你看get,辅助功能为std::tuple,你会发现以下过载:为什么得到的std ::的元组返回右值引用,而不是值

template< std::size_t I, class... Types > 
constexpr std::tuple_element_t<I, tuple<Types...> >&& 
get(tuple<Types...>&& t); 

换句话说,它返回当输入元组是右值引用本身时的右值引用。为什么不按价值回报,在函数体中调用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; 
} 

当运行时,该程序打印:

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()); 

它似乎并不像有任何额外的开销是否通过值或右值引用返回。我测试过这样的代码,看起来它会一直只执行一次移动构建。例如。如果我添加一个成员:

Hello(Hello &&) { std::cerr << "Moved" << std::endl; } 

我构建X如上,我的程序只“感动”一次不管我是否通过值或右值引用返回打印。

有没有很好的理由我错过了,或者这是一个疏忽?

注:这里有一个很好的相关问题:Return value or rvalue reference?。似乎认为在这种情况下价值回报通常是可取的,但STL出现的事实让我很好奇STL是否忽略了这种推理,或者如果他们有特殊的理由可能不适用通常。

编辑:有人提出这个问题是Is there any case where a return of a RValue Reference (&&) is useful?的重复。不是这种情况;这个答案建议通过右值引用返回,作为避免数据成员复制的一种方式。正如我上面详细讨论的那样,如果您首先拨打move,那么无论您是按价值还是按右值参考,复制都将被忽略。

+0

这不是远程重复。我的答案明确而详细地讨论了如何通过值返回并事先调用std :: move实现与从数据成员移动相同的效果。我的问题与两种方法的差异有关。我引用自己的问题与你引用的问题有更多的关系。在急于标注重复的内容之前,请仔细阅读。 –

+0

如果您按值返回 - 您创建一个副本。如果您移动它,则移动副本。如果你返回一个r值引用,你可以移动它,然后移动原始对象,或者不用移动字体处理它,那么它的行为就像常规引用。问题是如果你返回一个r值参考,你不会强制复制。 –

+0

为什么需要移动可施工性? – Columbo

回答

4

你可以用这个方法来创建一个悬空引用的例子非常有趣,但从例子中学习正确的教训很重要。

考虑一个非常简单的例子,不具有任何地方任何&&

const int &x = vector<int>(1) .front(); 

.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; 
} 

这将使优化的额外的超负荷:

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。你可以有100个嵌套调用concat那里,没有任何额外的结构。

所以,通过&&返回可以更快。因为,是的,移动速度比复制速度快,但如果可以避免这两种情况,速度还会更快。

总之,它是为了速度而完成的。考虑在一个元组内的元组中使用一个嵌套的get系列。此外,它允许它使用既没有复制也没有移动构造函数的类型。

这并没有引入任何有关生命的新风险。 vector<int>().front()“问题”不是一个新问题。

+2

我需要花更多时间与你答案充分吸收它。但请注意,具有讽刺意味的是,如果存在重载T front()&&,那么vector.front的示例将不会很慷慨。 –

+2

我想过提及这一点。是的,委员会应该在整个标准中加入'&'和'&&',就像你在评论中的例子。但我想这会破坏兼容性。另外,也许所有的方法默认都是'&',但这对于语言来说也是一个突破性的重大改变。 –

+0

概括地说,如果一个函数返回一个参数或一个参数的子对象,并且该参数是'&&',那么我认为没有理由不总是通过'&&'返回。我在这里太过分了吗? –

相关问题