Являются ли компиляторы достаточно умными для того, чтобы переменные std::move выходили из области видимости?
Рассмотрим следующий фрагмент кода:
std::vector<int> Foo() {
std::vector<int> v = Bar();
return v;
}
return v
равно O(1), так как NRVO пропустит копию, конструируя v
непосредственно в хранилище, куда возвращаемое значение функции в противном случае было бы перемещено или скопировано. Теперь рассмотрим функционально аналогичный код:
void Foo(std::vector<int> * to_be_filled) {
std::vector<int> v = Bar();
*to_be_filled = v;
}
Подобный аргумент можно привести здесь, так как *to_be_filled = v
предположительно может быть скомпилирован в O(1) move-assign, поскольку это локальная переменная, которая выходит из области видимости (компилятору должно быть достаточно легко проверить, что v
не имеет внешних ссылок в этом случае, и, таким образом, продвигает его к значению при последнем использовании). Это тот случай? Есть ли тонкая причина, почему нет?
Кроме того, создается впечатление, что этот шаблон можно распространить на любой контекст, где lvalue выходит за рамки:
void Foo(std::vector<int> * to_be_filled) {
if (Baz()) {
std::vector<int> v = Bar();
*to_be_filled = v;
}
...
}
Do / can / полезно / разумно ли ожидать, что компиляторы найдут шаблоны, такие как *to_be_filled = v
а затем автоматически оптимизировать их, чтобы принять семантику rvalue?
Редактировать:
g ++ 7.3.0 не выполняет никаких подобных оптимизаций в режиме -O3.
1 ответ
Компилятору не разрешается произвольно принимать решение о преобразовании имени lvalue в значение r, которое будет перемещено. Это может быть сделано только в том случае, если это позволяет стандарт C++. Такой как в return
заявление (и только когда его return <identifier>;
).
*to_be_filled = v;
всегда будет выполнять копию. Даже если это последнее утверждение, которое может получить доступ v
это всегда копия. Компиляторы не могут это изменить.
Насколько я понимаю, return v равен O(1), так как NRVO (по сути) превратит v в r-значение, которое затем использует std::vector move-constructor.
Это не так, как это работает. NRVO полностью исключит перемещение / копирование. Но способность к return <identifier>;
быть значением не является "оптимизацией". На самом деле это требование, чтобы компиляторы рассматривали их как значения.
У компиляторов есть выбор относительно копирования. Компиляторы не имеют выбора о том, что return <identifier>;
делает. Таким образом, вышесказанное либо не будет двигаться вообще (если происходит NRVO), либо будет перемещать объект.
Есть ли тонкая причина, почему нет?
Одна из причин, по которой это недопустимо, заключается в том, что расположение оператора не должно произвольно изменять то, что делает этот оператор. Увидеть, return <identifier>;
всегда будет двигаться от идентификатора (если это локальная переменная). Неважно, где это находится в функции. В силу того, что return
заявление, мы знаем, что если return
выполняется, ничего после того, как он будет выполнен.
Это не относится к произвольным утверждениям. Поведение выражения *to_be_filled = v;
не должен меняться в зависимости от того, где он находится в коде. Вы не должны быть в состоянии превратить движение в копию только потому, что добавили еще одну строку в функцию.
Другая причина в том, что произвольные утверждения могут очень быстро усложниться. return <identifier>;
очень просто; он копирует / перемещает идентификатор в возвращаемое значение и возвращает.
Напротив, что произойдет, если у вас есть ссылка на v
и это используется to_be_filled
как-то. Конечно, это не может произойти в вашем случае, но как насчет других, более сложных случаев? Последнее выражение может считаться ссылкой на удаленный объект.
Это намного сложнее сделать в return <identifier>;
случаев.