Упрощенная программа выглядит следующим образом
struct S {
S() {}
S(const S &) {}
S(S &&) = delete;
};
S x;
S y = false ? S() : x;
GCC и Clang прекрасно принимают, но последняя версия Visual Studio 2022 v17.10 отклоняет его с ошибкой:
error C2280: 'S::S(S &&)': attempting to reference a deleted function
Онлайн-демо: https://gcc.godbolt.org/z/xnc7x6W4v
Похоже, что MSVC сначала копирует значение x
во временный файл, а затем перемещает его во временный y
, в отличие от других компиляторов, которые не вызывают S(S &&)
.
Правильно ли поведение компилятора Microsoft соответствует стандарту C++?
@Gene Существует разница между удалением конструктора перемещения и его отсутствием вообще. Ожидается, что первый будет неправильно сформирован, если можно будет выбрать конструктор перемещения, а второй вернется к конструктору копирования.
Если конструктор перемещения не удален, MSVC выполнит копирование дважды: gcc.godbolt.org/z/7WKfvqoGW
@user17732522 user17732522 - существует тонкая разница между явным удалением и неявным созданием компилятором по умолчанию. Но результат тот же: конструктор перемещения недоступен, и в обоих случаях компилятору придется использовать конструктор копирования. Если вы думаете, что есть случай, когда объявление уже подавленного объекта перемещения по умолчанию явно удаленным делает программу некорректной, я хочу это увидеть.
@Gene Это вполне возможно, как и намерение = delete
. Пример: godbolt.org/z/r7nn6xvGv. Если вы удалите объявление конструктора перемещения, то конструктор перемещения вообще не будет объявлен, и он будет скомпилирован. Функция, определенная как удаленная, по-прежнему участвует в разрешении перегрузки как обычно, а не не участвует, как это сделала бы несуществующая функция.
@Fedor - это копирование
@user17732522 user17732522 - но это совершенно другое: вы явно требуете удалить функцию. Но мы всегда говорили о неявных преобразованиях.
@Gene Чем это отличается? S t = std::move(s);
— это инициализация копирования, которая также будет использовать неявное преобразование в инициализируемый тип. Разрешение перегрузки, используемое при формировании определяемой пользователем последовательности преобразования, разрешается в конструктор перемещения тогда и только тогда, когда он объявлен. Разрешение перегрузки неправильно сформировано, если выбранный конструктор удален, т. е. если конструктор перемещения определен как удаленный. Кроме того, в вопросе ОП неявного преобразования не произойдет, как я прокомментировал под одним из ответов.
@Gene Correction: в S t = std::move(s);
также нет неявного преобразования. Вместо этого разрешение перегрузки конструкторов выполняется немедленно согласно eel.is/c++draft/dcl.init#general-16.6.2.
@Gene • Но результат тот же, конструктор перемещения недоступен... Это неверно; разница в том, что для выбора наилучшего соответствия доступен удаленный конструктор перемещения. Эта тонкая разница является решающей.
@Eljay - здесь речь идет не о перегрузке функций, так что это не имеет значения.
Программа правильно сформирована, и msvc ошибается, отклоняя ее. Прежде всего обратите внимание, что S y = false ? S() : x;
— это инициализация копирования. Более того, результатом тернарного оператора здесь является prvalue.
Теперь результатом тернарного оператора является значение prvalue. Это видно из условного оператора :
Тип и категория значения условного выражения E1 ? E2 : E3 определяются по следующим правилам:
- В противном случае результатом будет prvalue. Если E2 и E3 не имеют одного и того же типа и любой из них имеет тип класса (возможно, с указанием cv), разрешение перегрузки выполняется с использованием встроенных кандидатов ниже, чтобы попытаться преобразовать операнды во встроенные типы. Если разрешение перегрузки не удается, программа имеет неверный формат. В противном случае применяются выбранные преобразования, и преобразованные операнды используются вместо исходных операндов на шаге 6.
Далее, начиная с C++17, существует обязательное исключение копирования, которое здесь применимо, поскольку у нас есть значение prvalue, указанное выше.
Из copy elision:
Начиная с C++17, значение prvalue не материализуется до тех пор, пока оно не понадобится, а затем оно создается непосредственно в хранилище конечного пункта назначения. Иногда это означает, что даже когда синтаксис языка визуально предполагает копирование/перемещение (например, инициализация копирования), копирование/перемещение не выполняется, а это означает, что типу вообще не обязательно иметь доступный конструктор копирования/перемещения. Примеры включают в себя:
- При инициализации объекта, когда выражение инициализатора представляет собой prvalue того же типа класса (игнорируя cv-квалификацию), что и тип переменной.
(выделено мной)
MSVC неправ; GCC и Clang верны.
MSVC неправильно применяет преобразование временной материализации (т. е. превращает S()
в значение x).
Если посмотреть на false ? S() : x;
, то это условный оператор, в котором оба возможных выражения относятся к одному и тому же типу класса, но S()
— это prvalue, а x
— lvalue.
Таким образом, результатом является prvalue ([expr.cond] p6).
Преобразование Lvalue в rvalue применяется к x
([expr.cond] p7), и объявление y
эффективно:
S y = false ? S() : S(x);
Соответствующий пункт: [expr.cond] p7.1.
Второй и третий операнды имеют один и тот же тип; результат имеет этот тип, и объект результата инициализируется с использованием выбранного операнда.
Начиная с C++17, конструктор перемещения здесь никогда не вызывается; вместо этого применяется [dcl.init.general] p16.6.1:
Если выражение инициализатора является значением prvalue, а неполная cv версия исходного типа относится к тому же классу, что и класс назначения, выражение инициализатора используется для инициализации целевого объекта.
S(x)
используется для инициализации целевого объекта y
. Конструктор перемещения никогда не вызывается, и временная материализация не происходит.
[expr.cond]/4 говорит: «если второй и третий операнд имеют разные типы». Здесь дело не в этом.
@user17732522 user17732522 спасибо, ты прав. Я это исправил. К счастью, вывод остался прежним; результатом является prvalue, а MSVC неверен.
Да, хотя мне интересно, является ли это дефектом стандарта, поскольку из этого следует, что constexpr S y = false ? S() : S(x);
будет неправильно сформировано согласно eel.is/c++draft/expr.const#5.9.
@user17732522 user17732522 хм, да, это кажется немного дефектным, потому что S(x)
, когда это делается напрямую, просто вызывается конструктор копирования, что не является проблемой для пустых классов, но когда это делается через преобразование lvalue в rvalue, это не допускается. Возможно, проблема связана с [expr.const].
@user17732522 github.com/cplusplus/CWG/issues/550
В reinterpret_cast
и const_cast
также есть преобразования lvalue в rvalue типа класса. В последнем случае это также проблема с константными выражениями. Помимо этого, существуют также преобразования lvalue в rvalue типа класса при вызове функции с переменным числом значений и для volatile
-квалифицированных выражений отброшенных значений. В этих случаях константные выражения не являются проблемой.
Но в целом мне кажется, что формулировка тщательно продумана, чтобы избежать преобразований lvalue в rvalue для типов классов. Преобразования Lvalue в rvalue также используются в eel.is/c++draft/dcl.dcl#dcl.attr.dependent-1 со смыслом, который, по-видимому, предполагает скалярные типы. Мне кажется, что они не должны применяться к типам классов.
Интересно то, что эта ошибка MSVC срабатывает только тогда, когда конструктор перемещения явно удаляется. Если вы хотите решить эту проблему, не объявляйте
S(S &&) = delete;
, конструктор перемещения по умолчанию уже подавлен (не создан). Сообщите об этом в Microsoft, они обычно быстро исправляют простые ошибки.