Подтвердить что ты не робот

Почему конструктор 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));
4b9b3361

Ответ 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: оно не возвращает тип класса по значению. Таким образом, не происходит никакого эликсира.