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[] для текущего типа вектора не является константным, или оператор + не является константным и т. Д.). Это позволит вам меньше беспокоиться.

Другие вопросы по тегам