Почему конструктор move не переводится по возможности с помощью функций `make_x()`?
Я не могу понять, почему в последнем случае это конструктор перемещения , вызываемый при включенном разрешении копирования (или даже обязательном, например, в С++ 17):
class X {
public:
X(int i) { std::clog << "converting\n"; }
X(const X &) { std::clog << "copy\n"; }
X(X &&) { std::clog << "move\n"; }
};
template <typename T>
X make_X(T&& arg) {
return X(std::forward<T>(arg));
}
int main() {
auto x1 = make_X(1); // 1x converting ctor invoked
auto x2 = X(X(1)); // 1x converting ctor invoked
auto x3 = make_X(X(1)); // 1x converting and 1x move ctor invoked
}
Какие правила мешают конструктору перемещения в этом случае?
UPDATE
Возможно, более простые случаи, когда вызываются конструкторы перемещения:
X x4 = std::forward<X>(X(1));
X x5 = static_cast<X&&>(X(1));
Ответы
Ответ 1
Два случая отличаются друг от друга, и важно понять, почему. С новой семантикой значений в С++ 17 основная идея заключается в том, что мы задерживаем процесс превращения prvalues в объекты как можно дольше.
template <typename T>
X make_X(T&& arg) {
return X(std::forward<T>(arg));
}
int main() {
auto x1 = make_X(1);
auto x2 = X(X(1));
auto x3 = make_X(X(1));
}
Для x1
первое выражение, имеющее тип X
, является телом в теле make_X
, которое в основном return X(1)
. Это значение типа X
. Мы инициализируем возвращаемый объект make_X
с этим значением prvalue, а затем make_X(1)
сам является значением класса X
, поэтому мы задерживаем материализацию. Инициализация объекта типа T
из prvalue типа T
означает прямое инициализацию из инициализатора, поэтому auto x1 = make_X(1)
сводится к просто X x1(1)
.
Для x2
редукция еще проще, мы просто применяем правило.
Для x3
сценарий отличается. У нас есть prvalue типа X
раньше (аргумент X(1)
) и что prvalue привязывается к ссылке! В момент привязки мы применяем временное преобразование материализации - это означает, что мы фактически создаем временный объект. Затем этот объект перемещается в возвращаемый объект, и мы можем сделать сокращение значения на последующем выражении полностью. Таким образом, это сводится к:
X __tmp(1);
X x3(std::move(__tmp));
У нас все еще есть один ход, но только один (мы можем преодолеть цепные движения). Это привязка к ссылке, которая требует существования отдельного объекта X
. Аргумент arg
и возвращаемый объект make_X
должны быть разными объектами, что означает, что должен произойти переход.
Для двух последних случаев:
X x4 = std::forward<X>(X(1));
X x5 = static_cast<X&&>(X(1));
В обоих случаях мы привязываем ссылку на prvalue, что опять-таки требует временного преобразования материализации. И тогда в обоих случаях инициализатор представляет собой значение xvalue, поэтому мы не получаем сокращения prvalue - у нас просто есть конструкция перемещения из x-значения, которое было материализованным временным объектом из prvalue.
Ответ 2
Потому что в выражении X(std::forward<T>(arg))
, даже если в последнем случае arg
является ссылкой, привязанной к временному, это все еще не временное. Внутри тела функции компилятор не может гарантировать, что arg
не привязан к lvalue. Подумайте, что произойдет, если конструктор перемещения будет удален, и вы выполните этот вызов:
auto x4 = make_X(std::move(x2));
x4
станет псевдонимом для x2
.
Правила для перемещения значения возвращаемого значения описаны в [class.copy]/32:
[...] Это разрешение операций копирования/перемещения, называемое копированием, разрешено в следующих случаях (которые могут быть объединены для устранения нескольких копий):
-
в операторе return в функции с типом возвращаемого класса, когда выражение является именем энергонезависимого автоматического объекта (кроме функции или параметра catch-clause) с тем же cv-неквалифицированным типом в качестве возвращаемого типа функции операцию копирования/перемещения можно опустить, построив автоматический объект непосредственно в возвращаемое значение функции
-
когда объект временного класса, который не был привязан к ссылке ([class.temporary]), будет скопирован/перенесен в объект класса с тем же CV-неквалифицированным типом, операция копирования/перемещения может быть опущено путем создания временного объекта непосредственно в цель пропущенной копии/перемещения
В вызове make_X(X(1))
выполняется копирование elision actualy, но только один раз:
- Первый X (1) создает временную привязку к
arg
.
- Затем
X(std::forward<T>(arg))
вызывает конструктор перемещения. arg
не является временным, поэтому второе правило выше не применяется.
- Затем в результате выражение
X(std::forward<T>(arg))
также должно быть перемещено, чтобы построить возвращаемое значение, но этот шаг устранен.
О вашем UPDATE, std::forward
вызывают материализацию временного X(1)
, связанного с xvalue: возврат std::forward
. Это возвращаемое значение xvalue не является временным, поэтому copy/elision больше не применимо.
Опять же, что произойдет в этом случае, если произойдет переход. (С++ грамматика не является контекстуальной):
auto x7 = std::forward<X>(std::move(x2));
Nota: После того, как я увидел новый ответ о С++ 17, я хотел добавить в замешательство.
В С++ 17 определение prvalue
заключается в том, что было изменено то, что в вашем примере кода больше нет конструктора перемещения. Вот пример result code GCC с опцией fno-elide-constructors
в С++ 14, а затем в С++ 17:
#c++ -std=c++14 -fno-elide-constructors | #c++ -std=c++17 -fno-elide-constructors
main: | main:
sub rsp, 24 | sub rsp, 24
mov esi, 1 | mov esi, 1
lea rdi, [rsp+15] | lea rdi, [rsp+12]
call X::X(int) | call X::X(int)
lea rsi, [rsp+15] | lea rdi, [rsp+13]
lea rdi, [rsp+14] | mov esi, 1
call X::X(X&&) | call X::X(int)
lea rsi, [rsp+14] | lea rdi, [rsp+15]
lea rdi, [rsp+11] | mov esi, 1
call X::X(X&&) | call X::X(int)
lea rdi, [rsp+14] | lea rsi, [rsp+15]
mov esi, 1 | lea rdi, [rsp+14]
call X::X(int) | call X::X(X&&)
lea rsi, [rsp+14] | xor eax, eax
lea rdi, [rsp+15] | add rsp, 24
call X::X(X&&) | ret
lea rsi, [rsp+15]
lea rdi, [rsp+12]
call X::X(X&&)
lea rdi, [rsp+13]
mov esi, 1
call X::X(int)
lea rsi, [rsp+13]
lea rdi, [rsp+15]
call X::X(X&&)
lea rsi, [rsp+15]
lea rdi, [rsp+14]
call X::X(X&&)
lea rsi, [rsp+14]
lea rdi, [rsp+15]
call X::X(X&&)
xor eax, eax
add rsp, 24
ret
Ответ 3
Чтобы упростить свой пример:
auto x1 = make_X(1); // converting
auto x2 = X(X(1)); // converting
auto x4 = X(std::forward<X>(X(1))); // converting + move
Из cppreference скопировать документацию о разрешении (внимание мое):
До С++ 17:
В следующих случаях компиляторы разрешены, но не требуется опускать копирование и перемещение (поскольку С++ 11) построение объекты класса...
- Если функция возвращает тип класса по значению, а возврат выражение выражения - это название нелетучего объекта с время автоматического хранения, которое не является параметром функции, или catch, и который имеет тот же тип (игнорируя верхний уровень cv-квалификации) в качестве возвращаемого типа функции, то copy/move (поскольку С++ 11) опущен. Когда этот локальный объект построена непосредственно в хранилище, где иначе возвращаемое значение функции было бы перемещено или скопировано в. Эта вариант эмиссионного копирования называется NRVO, "named return value оптимизация".
Так как С++ 17:
При следующих обстоятельствах компиляторы должны опускать копирование и перемещение...
a) При инициализации, если выражение инициализатора является prvalue и cv-неквалифицированная версия типа источника - это тот же класс, что и класс адресата, выражение инициализатора используется для инициализировать объект назначения:
T x = T(T(T())); // only one call to default constructor of T, to initialize x
b) В вызове функции, если операндом оператора return является prvalueи возвращаемый тип функции такой же, как тип этого prvalue.
T f() { return T{}; }
T x = f(); // only one call to default constructor of T, to initialize x
T* p = new T(f()); // only one call to default constructor of T, to initialize *p
В любом случае std::forward
не соответствует требованиям, поскольку в результате получается значение xvalue, а не значение prvalue: оно не возвращает тип класса по значению. Таким образом, не происходит никакого эликсира.