C ++在完全转发的同一语句中多次访问rvalue引用

时间:2014-11-13 03:22:09

标签: c++ c++14 rvalue-reference perfect-forwarding

以下代码是否安全?特别是,如果vec是一个右值引用,那么最后一行是否应该执行它应该做的事情(即vec的元素被正确求和的递归)?

template<typename VectorType>
auto recurse(VectorType&& vec, int i)
{
     if(i<0)
          return decltype(vec[i])();
     return vec[i] + recurse(std::forward<VectorType>(vec), i-1); //is this line safe?
}

我的怀疑与以下事实有关:自the order of evaluation is unspecified以来,矢量可能在评估operator[]之前被移动,因此后者可能会失败。

这种恐惧是否合理,或者是否存在一些阻止这种情况的规则?

3 个答案:

答案 0 :(得分:2)

请考虑以下事项:

std::vector<int> things;

// fill things here

const auto i = static_cast<int>(things.size()) - 1;

// "VectorType &&" resolves to "std::vector<int> &&" -- forwarded indirectly
recurse(move(things), i);

// "VectorType &&" resolves to "std::vector<int> &" -- also forwarded indirectly
recurse(things, i);

// "VectorType &&" resolves to "const std::vector<int> &" -- again, forwarded indirectly
recurse(static_cast<const std::vector<int> &>(things), i);

即使在上面示例中对recurse的所有3次调用之后,things向量仍然完好无损。

如果递归更改为:

return vec[i] + recurse(forward<VectorType>(vec), --i);

结果将是未定义的,因为可以按任意顺序评估vec[i]--i

函数调用类似于序列点:必须在调用函数之前计算参数表达式的结果。然而,发生这种情况的 order 是未定义的 - 即使对于同一语句中的子表达式也是如此。

在递归语句中强制构造中间符也会导致未定义的行为。

例如:

template<typename VectorType>
auto recurse(VectorType &&vec, int i)
{
    using ActualType = typename std::decay<VectorType>::type;
    if(i < 0){
        return decltype(vec[i]){};
    }
    return vec[i] + recurse(ActualType{forward<VectorType>(vec)}, i - 1);
}

此处,vec[i]ActualType{forward<VectorType>(vec)}可以按任意顺序进行评估。后者将复制构造或移动构造ActualType的新实例,这就是未定义的原因。

摘要

是的,您的示例将对向量的内容求和。

编译器没有理由构造一个中间体,因此recurse的连续调用每个都会接收对同一个不变的实例的间接引用。

附录

正如评论中指出的那样,非const operator[]可能会改变VectorType的实例,无论它是什么。在这种情况下,递归的结果将是未定义的。

答案 1 :(得分:1)

你是对的,vec[i]的评估和递归调用是不确定的(他们实际上不能重叠,但可能发生在两个可能的顺序中)。

我不明白你为什么要参加右值参考,因为你实际上并没有 vec移动。

除非VectorTypeoperator[](index_type) &&,否则我不会看到不确定的执行如何导致失败。 (另一方面,发现无限递归ildjarn会导致失败)。实际上,operator[](index_type) &&会导致所有执行命令失败。

此功能应该可以使用const&参数类型正常工作,然后您就不会担心。

答案 2 :(得分:1)

std::forward不会自己改变你的对象。它会将vec转换为在调用recurse的父调用时推导出的相同值类别。就此而言,std::move本身也不会改变其论点。

但是,转换为右值类别可以间接地使突变发生(这是重点,它可以基于改变从对象移动的一些优化)。如果正在调用的函数的rvalue重载会改变其参数。

在这种情况下,似乎没有人在改变事物(只要vec [i]或操作员+不会改变事物),所以我认为无论评估的顺序是什么,你都会安全

正如另一个回答者指出的那样,通过const ref意味着编译器会抱怨突变是否可能发生在某处(例如,当前向量类型的operator []是非常量,或者运算符+是非常量等)。这样可以减少您的担忧。