Переместить исключение в явных функциях-членах объекта

Если кто-то вызывает явную функцию-член объекта временного объекта, должно ли перемещение временного объекта быть исключено в явном параметре объекта?

Рассмотрим следующий пример, где struct A удален конструктор перемещения, а f(this A) вызывается для временного объекта A:

struct A {
    A() {}
    A(A&&) = delete;
    void f(this A) {}
};

int main() {
    A{}.f();
}

Программа принимается в GCC, но и Clang, и MSVC ее отвергают:

вызов удаленного конструктора «A»

ошибка C2280: «A::A(A &&)»: попытка сослаться на удаленную функцию

Онлайн-демо: https://gcc.godbolt.org/z/rbv14cnz5

Какой компилятор здесь прав?

Я думаю, что GCC здесь прав. Ничто не говорит о том, что подразумеваемый аргумент объекта должен вести себя иначе, чем любой другой аргумент. Единственное исключение, где принудительное преобразование prvalue в xvalue, — это неявные объектные функции (eel.is/c++draft/expr.call#6.sentence-4).

user17732522 23.07.2024 21:42

На самом деле, я был бы удивлён, если бы GCC здесь не был прав: одна из целей явных параметров объекта по значению — сделать цепочку вызовов методов эффективной. Не должно быть лишних переходов от одного возвращаемого значения к параметру следующего вызова в цепочке.

user17732522 23.07.2024 21:46

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

Nicol Bolas 23.07.2024 21:54
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
3
121
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

GCC прав. К сожалению, стандарт не содержит явного указания на этот факт. Скорее, именно отсутствие специальных формулировок для явных параметров объекта в различных местах делает вашу программу корректной.

Выражение A{}.f() состоит из выражения A{}, встроенного в выражение доступа к члену класса A{}.f, которое в свою очередь встроено в выражение вызова функции A{}.f(). Начиная с доступа к членам, в [expr.ref] нет формулировки, требующей, чтобы операнд-получатель был или был преобразован в glvalue в этом случае. Пункт 2 накладывает это ограничение только на нестатические элементы данных:

Для первого варианта (точка), если выражение id [здесь f] называет статический член или перечислитель, ...; если выражение id называет нестатический элемент данных, первое выражение должно быть значением glvalue. ...

В пункте (7.3), который обрабатывает доступ к функциям, просто говорится:

Если E2 [здесь f] является набором перегрузки, выражение должно быть (возможно, в скобках) левым операндом вызова функции-члена (...), а разрешение перегрузки функции (...) используется для выбора функция, к которой относится E2. Тип E1.E2 [здесь E1 = A{}] — это тип E2, а E1.E2 относится к функции, на которую ссылается E2.

  • Если E2 относится к статической функции-члену,...
  • В противном случае (когда E2 относится к нестатической функции-члену), E1.E2 является значением prvalue.

Опять же, важной частью является отсутствие каких-либо формулировок, ограничивающих A{} быть glvalue.

Переходя к разрешению перегрузки, соответствующее правило: [over.call.func]/2:

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

Итак, в вашем случае A{}.f() становится f(A{}) для целей разрешения перегрузки, тогда как рассматриваемый набор перегрузок — это просто f(this A). Разрешение перегрузки должно быть успешным. Сообщения об ошибках Clang и MSVC позволяют предположить, что они успешно зашли так далеко.

Явные параметры объекта не обрабатываются специально правилом, определяющим инициализацию параметров функции. Неявные параметры объекта представляют собой особый случай. [expr.call]/6 гласит:

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

Опять же, предложение, которое может привести к сбою вашего кода (последнее), в данном случае не применимо, поскольку f не является неявной функцией-членом объекта.

Наконец, при вычислении вызова функции параметр this A для f должен быть инициализирован выражением prvalue A{}. Это удается без участия конструктора перемещения с помощью [dcl.init.general]/16.6

В противном случае, если тип назначения является типом класса:

  • Если выражение инициализатора является значением prvalue, а неполная cv версия исходного типа совпадает с типом назначения, выражение инициализатора используется для инициализации целевого объекта.
  • ...

(В конце концов, выражение A{} не использует конструктор перемещения.)


В стороне отмечу, что Ваша формулировка вопроса не совсем корректна. A{} не является «временным»; это ценно. prvalue — это выражение, которое при вычислении инициализирует заданную часть памяти объектом. Когда функция void g(A); вызывается g(A{}), отсутствие хода не следует рассматривать как «исключение». Мышление в терминах «исключения» имеет смысл только при сравнении C++11 и C++03. Начиная с C++11, возможности уйти просто нет. Ваш вопрос лучше всего написать: «Когда функция с явным параметром объекта по значению вызывается по prvalue типа класса, является ли она временной материализацией и перемещением?» Я отвечаю: «Нет, значение prvalue напрямую инициализирует параметр, и нет никаких временных и никаких перемещений».

Следует отметить, что утверждение в [expr.ref]/2 изменилось сравнительно недавно. N4590 (от мая 2023 г.) просто говорит: «Для первого варианта (точки) первое выражение должно быть glvalue». А заявление о том, что это «должно быть значением gl», провоцирует временную материализацию. Я не знаю, когда был опубликован C++23, но это может быть какое-то исправление дефекта или преднамеренное изменение после C++23, которое не приняли другие компиляторы.

Nicol Bolas 23.07.2024 23:41
Ответ принят как подходящий

Это CWG2813. Как отмечается в описании проблемы, формулировка в C++23 требует наличия glvalue для левого операнда оператора точки, а это означает, что если левый операнд является prvalue, его необходимо сначала преобразовать в glvalue (материализовать), что предотвращает копирование/перемещение исключения. Этот результат нежелателен; мы хотели бы, чтобы prvalue напрямую инициализировал явный параметр объекта. Итак, в формулировку было внесено изменение и он был утвержден как ДР. Компиляторам потребуется некоторое время, чтобы наверстать упущенное, но цель состоит в том, чтобы этот пример был принят.

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