C++ множественный доступ к rvalue-ссылке в том же операторе, что и совершенная пересылка
Следующий код безопасен? Особенно если vec
является ссылкой на rvalue, делает ли последняя строка то, что должна (а именно рекурсию, в которой элементы 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?
}
Мои сомнения связаны с тем фактом, что, поскольку порядок оценки не указан, вектор мог быть перемещен раньше operator[]
оценивается, и, следовательно, последний может потерпеть неудачу.
Оправдан ли этот страх, или есть какое-то правило, которое этому препятствует?
3 ответа
Учтите следующее:
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);
Даже после всех 3 звонков recurse
в приведенном выше примере things
вектор по-прежнему не поврежден.
Если рекурсия была изменена на:
return vec[i] + recurse(forward<VectorType>(vec), --i);
результаты будут неопределенными, так как либо vec[i]
или же --i
можно оценить в любом порядке.
Вызовы функций похожи на точки последовательности: результаты выражений аргументов должны быть вычислены до вызова функции. Однако порядок, в котором это происходит, не определен - даже в отношении подвыражений в одном и том же утверждении.
Принудительное создание промежуточного объекта в операторе рекурсии также приведет к неопределенному поведению.
Например:
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
каждый получает косвенную ссылку на один и тот же неизменный экземпляр.
добавление
Как указано в комментарии, это возможно для неконстантных operator[]
мутировать случай VectorType
Что бы это ни было. В этом случае результат рекурсии будет неопределенным.
Вы правы, оценка vec[i]
и рекурсивный вызов неопределенно упорядочен (они не могут фактически перекрываться, но могут происходить в любом из двух возможных порядков).
Я не понимаю, почему вы берете ссылку на rvalue, потому что вы никогда не уходите от vec
,
Если не VectorType
имеет operator[](index_type) &&
Я не вижу, как неопределенное выполнение на самом деле приводит к провалу. (С другой стороны, обнаруженная бесконечная рекурсия ildjarn приведет к сбою). На самом деле, operator[](index_type) &&
приведет к неудаче со всеми исполнительными приказами.
Эта функция должна нормально работать с const&
Тип параметра, и тогда вы не будете беспокоиться.
std::forward
не будет мутировать ваш объект сам по себе. Он приведёт vec к той же категории значений, которая была выведена при выполнении родительского вызова recurse. В этом отношении, std::move
также не будет мутировать сам по себе.
Однако приведение к категории rvalue может косвенно включить мутацию (и в этом вся суть, она дает возможность некоторых оптимизаций, основанных на мутации перемещенного объекта). Если перегрузка rvalue для функции, которую вы вызываете, изменяет ее аргумент.
В этом случае кажется, что никто не мутирует (если vec[i] или operator+ не мутирует), поэтому я думаю, что вы будете в безопасности здесь, независимо от порядка вычисления.
Как указал другой ответчик, передача по const ref означает, что компилятор будет жаловаться, если где-то может произойти мутация (например, operator[] для текущего типа вектора не является константным, или оператор + не является константным и т. Д.). Это позволит вам меньше беспокоиться.