以下代码是否安全?特别是,如果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[]
之前被移动,因此后者可能会失败。
这种恐惧是否合理,或者是否存在一些阻止这种情况的规则?
答案 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
移动。
除非VectorType
有operator[](index_type) &&
,否则我不会看到不确定的执行如何导致失败。 (另一方面,发现无限递归ildjarn会导致失败)。实际上,operator[](index_type) &&
会导致所有执行命令失败。
此功能应该可以使用const&
参数类型正常工作,然后您就不会担心。
答案 2 :(得分:1)
std::forward
不会自己改变你的对象。它会将vec转换为在调用recurse的父调用时推导出的相同值类别。就此而言,std::move
本身也不会改变其论点。
但是,转换为右值类别可以间接地使突变发生(这是重点,它可以基于改变从对象移动的一些优化)。如果正在调用的函数的rvalue重载会改变其参数。
在这种情况下,似乎没有人在改变事物(只要vec [i]或操作员+不会改变事物),所以我认为无论评估的顺序是什么,你都会安全
正如另一个回答者指出的那样,通过const ref意味着编译器会抱怨突变是否可能发生在某处(例如,当前向量类型的operator []是非常量,或者运算符+是非常量等)。这样可以减少您的担忧。