Перемещение после копирования при присвоении результата условного оператора

Упрощенная программа выглядит следующим образом

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++?

Интересно то, что эта ошибка MSVC срабатывает только тогда, когда конструктор перемещения явно удаляется. Если вы хотите решить эту проблему, не объявляйте S(S &&) = delete;, конструктор перемещения по умолчанию уже подавлен (не создан). Сообщите об этом в Microsoft, они обычно быстро исправляют простые ошибки.

Gene 08.06.2024 20:01

@Gene Существует разница между удалением конструктора перемещения и его отсутствием вообще. Ожидается, что первый будет неправильно сформирован, если можно будет выбрать конструктор перемещения, а второй вернется к конструктору копирования.

user17732522 08.06.2024 20:39

Если конструктор перемещения не удален, MSVC выполнит копирование дважды: gcc.godbolt.org/z/7WKfvqoGW

Fedor 08.06.2024 20:48

@user17732522 user17732522 - существует тонкая разница между явным удалением и неявным созданием компилятором по умолчанию. Но результат тот же: конструктор перемещения недоступен, и в обоих случаях компилятору придется использовать конструктор копирования. Если вы думаете, что есть случай, когда объявление уже подавленного объекта перемещения по умолчанию явно удаленным делает программу некорректной, я хочу это увидеть.

Gene 08.06.2024 20:54

@Gene Это вполне возможно, как и намерение = delete. Пример: godbolt.org/z/r7nn6xvGv. Если вы удалите объявление конструктора перемещения, то конструктор перемещения вообще не будет объявлен, и он будет скомпилирован. Функция, определенная как удаленная, по-прежнему участвует в разрешении перегрузки как обычно, а не не участвует, как это сделала бы несуществующая функция.

user17732522 08.06.2024 20:58

@Fedor - это копирование

Gene 08.06.2024 20:58

@user17732522 user17732522 - но это совершенно другое: вы явно требуете удалить функцию. Но мы всегда говорили о неявных преобразованиях.

Gene 08.06.2024 21:00

@Gene Чем это отличается? S t = std::move(s); — это инициализация копирования, которая также будет использовать неявное преобразование в инициализируемый тип. Разрешение перегрузки, используемое при формировании определяемой пользователем последовательности преобразования, разрешается в конструктор перемещения тогда и только тогда, когда он объявлен. Разрешение перегрузки неправильно сформировано, если выбранный конструктор удален, т. е. если конструктор перемещения определен как удаленный. Кроме того, в вопросе ОП неявного преобразования не произойдет, как я прокомментировал под одним из ответов.

user17732522 08.06.2024 21:04

@Gene Correction: в S t = std::move(s); также нет неявного преобразования. Вместо этого разрешение перегрузки конструкторов выполняется немедленно согласно eel.is/c++draft/dcl.init#general-16.6.2.

user17732522 08.06.2024 21:12

@Gene • Но результат тот же, конструктор перемещения недоступен... Это неверно; разница в том, что для выбора наилучшего соответствия доступен удаленный конструктор перемещения. Эта тонкая разница является решающей.

Eljay 09.06.2024 13:48

@Eljay - здесь речь идет не о перегрузке функций, так что это не имеет значения.

Gene 09.06.2024 20:22
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
15
11
545
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Программа правильно сформирована, и msvc ошибается, отклоняя ее. Прежде всего обратите внимание, что S y = false ? S() : x; — это инициализация копирования. Более того, результатом тернарного оператора здесь является prvalue.

Теперь результатом тернарного оператора является значение prvalue. Это видно из условного оператора :

Тип и категория значения условного выражения E1 ? E2 : E3 определяются по следующим правилам:

    1. В противном случае результатом будет 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 08.06.2024 20:21

@user17732522 user17732522 спасибо, ты прав. Я это исправил. К счастью, вывод остался прежним; результатом является prvalue, а MSVC неверен.

Jan Schultke 08.06.2024 21:05

Да, хотя мне интересно, является ли это дефектом стандарта, поскольку из этого следует, что constexpr S y = false ? S() : S(x); будет неправильно сформировано согласно eel.is/c++draft/expr.const#5.9.

user17732522 08.06.2024 21:06

@user17732522 user17732522 хм, да, это кажется немного дефектным, потому что S(x), когда это делается напрямую, просто вызывается конструктор копирования, что не является проблемой для пустых классов, но когда это делается через преобразование lvalue в rvalue, это не допускается. Возможно, проблема связана с [expr.const].

Jan Schultke 08.06.2024 22:24

@user17732522 github.com/cplusplus/CWG/issues/550

Jan Schultke 08.06.2024 23:06

В reinterpret_cast и const_cast также есть преобразования lvalue в rvalue типа класса. В последнем случае это также проблема с константными выражениями. Помимо этого, существуют также преобразования lvalue в rvalue типа класса при вызове функции с переменным числом значений и для volatile-квалифицированных выражений отброшенных значений. В этих случаях константные выражения не являются проблемой.

user17732522 09.06.2024 00:09

Но в целом мне кажется, что формулировка тщательно продумана, чтобы избежать преобразований lvalue в rvalue для типов классов. Преобразования Lvalue в rvalue также используются в eel.is/c++draft/dcl.dcl#dcl.attr.dependent-1 со смыслом, который, по-видимому, предполагает скалярные типы. Мне кажется, что они не должны применяться к типам классов.

user17732522 09.06.2024 00:11

Другие вопросы по теме

Похожие вопросы

Почему `std::integral_constant` имеет `::type`, который ссылается на себя?
Агрегатная инициализация, которая имеет инициализатор члена, когда значения инициализации меньше количества членов
Макросы LITTLE_ENDIAN, BIG_ENDIAN и BYTE_ORDER должны загрязнять мое глобальное пространство имен с помощью GCC?
Что происходит, когда сопрограмма возвращается в приостановленную сопрограмму?
Boost Asio: исполнители в сопрограммах C++20
Можно ли рассматривать reinterpret_cast как небезопасный обходной путь, когда динамический_cast недоступен?
Как предотвратить возникновение противоречивых ошибок в конфигурациях отладки и выпуска для «недоступного кода» и «не все пути возвращают значение»
Можно ли в Visual Studio Code автоматически переключаться на панель «Проблемы», когда при сборке возникают ошибки?
Как сборка этой функции реализует условие?
Каков переносимый способ приведения к символу и обратно, сохраняя тот же битовый шаблон?