Если кто-то вызывает явную функцию-член объекта временного объекта, должно ли перемещение временного объекта быть исключено в явном параметре объекта?
Рассмотрим следующий пример, где 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.stuff
должно проявлять временный выход из prvalue
, прежде чем делать то, что находится в stuff
. Но беглое изучение стандарта нигде этого не дает.
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, которое не приняли другие компиляторы.
Это CWG2813. Как отмечается в описании проблемы, формулировка в C++23 требует наличия glvalue для левого операнда оператора точки, а это означает, что если левый операнд является prvalue, его необходимо сначала преобразовать в glvalue (материализовать), что предотвращает копирование/перемещение исключения. Этот результат нежелателен; мы хотели бы, чтобы prvalue напрямую инициализировал явный параметр объекта. Итак, в формулировку было внесено изменение и он был утвержден как ДР. Компиляторам потребуется некоторое время, чтобы наверстать упущенное, но цель состоит в том, чтобы этот пример был принят.
Я думаю, что GCC здесь прав. Ничто не говорит о том, что подразумеваемый аргумент объекта должен вести себя иначе, чем любой другой аргумент. Единственное исключение, где принудительное преобразование prvalue в xvalue, — это неявные объектные функции (eel.is/c++draft/expr.call#6.sentence-4).