Запрещает ли С++ 17 копирование эликсирования в случае, когда С++ 14 разрешил это?

Рассмотрим следующее:

struct X {
    X() {}
    X(X&&) { puts("move"); }
};
X x = X();

В С++ 14 переход может быть отменен, несмотря на то, что конструктор перемещения имеет побочные эффекты благодаря [class.copy]/31,

Это разрешение операций копирования/перемещения... разрешено в следующих случаях... когда временный объект класса, который не был привязан к ссылке (12.2), будет скопирован/перемещен к объекту класса с тем же самым cv-неквалифицированным типом

В С++ 17 эта пуля была удалена. Вместо этого движение, как гарантируется, будет отменено благодаря [dcl.init]/17.6.1:

Если выражение инициализатора является значением prvalue, а cv-unqualified версия типа источника - то же самое класс как класс адресата, выражение инициализатора используется для инициализации адресата объект. [Пример: T x = T(T(T())); вызывает конструктор по умолчанию T для инициализации x. - конец пример]

До сих пор факты, о которых я говорил, хорошо известны. Но теперь измените код так, чтобы он читал:

X x({});

В С++ 14 выполняется разрешение перегрузки, а {} преобразуется во временный тип x с использованием конструктора по умолчанию, а затем перемещается в x. Правила исключения копий позволяют исключить этот шаг.

В С++ 17 разрешение перегрузки одинаков, но теперь [dcl.init]/17.6.1 не применяется, и пуля с С++ 14 больше не существует. Не существует выражения инициализатора, так как инициализатор представляет собой список бит-init. Вместо этого оказывается, что применяется [dcl.init]/(17.6.2): ​​

В противном случае, если инициализация является прямой инициализацией или если она является копией-инициализацией, где cv-неквалифицированная версия типа источника - это тот же класс, что и производный класс класса назначения, конструкторы. Соответствующие конструкторы перечислены (16.3.1.3), и лучший выбирается с помощью разрешения перегрузки (16.3). Выбранный таким образом конструктор называется для инициализации объекта с выражением инициализатора или списком выражений в качестве аргумента (ов). Если нет конструктор, или разрешение перегрузки неоднозначно, инициализация плохо сформирована.

Кажется, что требуется вызвать конструктор перемещения, и если в каком-либо другом стандарте есть правило, в котором говорится, что это нормально, я не знаю, где он находится.

Ответ 1

Как T.C. указывает, что это похоже на CWG 2327:

Рассмотрим пример типа:

struct Cat {};
struct Dog { operator Cat(); };

Dog d;
Cat c(d);

Это относится к 11.6 [dcl.init] bullet 17.6.2:

В противном случае, если инициализация является прямой инициализацией или если она является копией-инициализацией, где cv-неквалифицированная версия типа источника является тем же классом, что или производным классом класса назначения, то конструкторы считается. Соответствующие конструкторы перечислены (16.3.1.3 [over.match.ctor]), а лучший выбирается с помощью разрешения перегрузки (16.3 [over.match]). Выбранный таким образом конструктор вызывается для инициализации объекта с выражением инициализатора или списком выражений в качестве аргумента (ов). Если конструктор не применяется или разрешение перегрузки неоднозначно, инициализация плохо сформирована.

Разрешение перегрузки выбирает конструктор перемещения Cat. Инициализация параметра Cat&& конструктора приводит к временной, для 11.6.3 [dcl.init.ref] bullet 5.2.1.2. Это исключает возможность получения копии для этого случая.

Это похоже на надзор в изменении формулировки для гарантированного копирования. Мы должны, по-видимому, одновременно рассматривать как конструкторы, так и функции преобразования в этом случае, как и для копирования-инициализации, но нам нужно убедиться, что не вводит никаких новых проблем или двусмысленностей.

Что делает эту проблему той же самой проблемой, что у нас есть инициализатор (в OP, {}, в этом примере, d), который неправильный тип - нам нужно преобразовать его в нужный тип (X или Cat), но чтобы выяснить, как это сделать, нам нужно выполнить разрешение перегрузки. Это уже приводит нас к конструктору перемещения - где мы привязываем этот параметр ссылки rvalue к новому объекту, который мы только что создали, чтобы это произошло. На данный момент это слишком поздно, чтобы ускользнуть. Мы уже здесь. Мы не можем... создать резервную копию, ctrl-z, отменить отмену, нормально начать.

Как я уже упоминал в комментариях, я не уверен, что это было другим в С++ 14. Чтобы оценить X x({}), нам нужно построить X, который мы привязываем к эталонному параметру rvalue конструктора перемещения - мы не можем преодолеть ход в этой точке, привязка ссылок происходит, прежде чем мы даже узнаем мы делаем шаг.