Подтвердить что ты не робот

Конструктор вызывал оператор return

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

class X {
public:
    X() = default;
    X(const X&) = default;
    X(X&&) = delete;
};

X foo() {
    X result;
    return result;
}

int main() {
    foo();
}

Clang и GCC не согласны с тем, действительно ли эта программа. GCC пытается вызвать конструктор перемещения при инициализации временного во время вызова foo(), который был удален, что приводит к ошибке компиляции. Кланг обрабатывает это просто отлично, даже с -fno-elide-constructors.

Может ли кто-нибудь объяснить, почему GCC разрешено вызывать конструктор перемещения в этом случае? Не result lvalue?

4b9b3361

Ответ 1

Из [class.copy.elision]:

В следующих контекстах копирования-инициализации вместо операции копирования может использоваться операция перемещения: Если выражение в операторе return является (возможно, в скобках) id-выражением, которое называет объект с автоматической продолжительностью хранения объявлено в теле [...]

разрешение перегрузки для выбора конструктора для копии сначала выполняется так, как если бы объект был обозначен rvalue. Если первое разрешение перегрузки завершилось с ошибкой или не было выполнено или , если тип первого параметра выбранного конструктора не является ссылкой rvalue на тип объекта (возможно, cv- квалифицированный), разрешение перегрузки выполняется снова, считая объект как lvalue.

В return result; мы точно в этом случае упомянуты в этом параграфе - result - это id-выражение, обозначающее объект с автоматическим временем хранения, объявленным в теле. Итак, мы сначала выполняем перегрузочное разрешение, как если бы оно было rvalue.

Разрешение перегрузки найдет двух кандидатов: X(X const&) и X(X&&). Последний является предпочтительным.

Теперь, что означает, что для разрешения перегрузки произойдет сбой? Из [over.match]/3:

Если наилучшая жизнеспособная функция существует и уникальна, разрешение перегрузки преуспевает и дает результат в результате. В противном случае разрешение перегрузки не выполняется, а вызов плохо сформирован.

X(X&&) - уникальная, наиболее эффективная функция, поэтому разрешение перегрузки заканчивается успешно. Этот контекст контекста копии имеет для нас еще один дополнительный критерий, но мы его удовлетворяем, потому что этот кандидат имеет тип первого параметра, который (возможно, cv-квалифицированный) имеет значение rvalue для X. Обе коробки проверены, поэтому мы останавливаемся там. Мы не будем снова выполнять перегрузочное разрешение как lvalue, мы уже выбрали нашего кандидата: конструктор перемещения.

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

Это ошибка clang 31025.

Ответ 2

Я собираюсь процитировать С++ 17 (n4659), так как формулировка там самая ясная, но предыдущие версии говорят то же самое, что менее понятно для меня. Вот [class.copy.elision]/3, основное внимание:

В следующих контекстах копирования-инициализации операция перемещения может вместо операции копирования:

  • Если выражение в операторе return является (возможно, в скобках) id-выражением, которое называет объект с автоматическим длительность хранения, объявленная в теле или параметре-объявление-предложение самой внутренней охватывающей функции или лямбда-выражения, или

  • [...]

разрешение перегрузки для выбора конструктора для копии сначала выполняются так, как если бы объект был обозначен rvalue. Если первое разрешение перегрузки выходит из строя или не выполняется, или если тип первого параметра выбранного конструктора не является значением rvalue ссылка на тип объекта (возможно, с квалификацией cv), перегрузка разрешение выполняется снова, считая объект как lvalue.[Примечание. Это двухступенчатое разрешение перегрузки должно выполняться независимо от того, произойдет ли копирование. Он определяет конструктор, который будет вызываться, если исключение не выполняется, а выбранный конструктор должен быть доступен, даже если вызов отменен. - конец примечание]

Вот почему на самом деле и Clang, и GCC будут пытаться сначала вызвать движение c'tor. Разница в поведении заключается в том, что Кланг придерживается текста, выделенного полужирным шрифтом по-разному. Было обнаружено перегрузочное разрешение, найдено движение c'tor, но назовем его плохо сформированным. Итак, Кланг выполняет его снова и находит копию c'tor.

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

Я действительно верю, что Кланг здесь прав, по духу, если что-то еще. Предполагаемое перемещение возвращаемого значения и копирование в качестве резерва - это предполагаемое поведение этой оптимизации. Я чувствую, что GCC не должен останавливаться, потому что он нашел удаленный ход c'tor. Ни один компилятор не был бы, если бы это был дефолтный движок c'tor, который был определен удаленным. Стандарт осознает потенциальную проблему в этом случае ([over.match.funcs]/8):

Специальная функция перемещения по умолчанию ([class.copy]), которая определяется как исключение исключено из набора функций-кандидатов во всех контексты.

Ответ 3

result не является lvalue после возврата из foo(), но является prvalue, поэтому ему разрешено вызывать конструктор перемещения.

От CppReference:

Следующие выражения представляют собой выражения prvalue:

  • вызов функции или перегруженное выражение оператора , тип возврата которого не ссылается, например str.substr(1, 2), str1 + str2 или it++

Возможно, что Clang обнаружил, что возвращаемое значение не используется и отбрасывает его напрямую, а GCC - нет.