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

Недавно я наткнулся на этот ответ, в котором описывается, как инициализировать std::array элементов, не соответствующих стандарту. Я не был так удивлен, потому что этот ответ явно не делает никаких построений по умолчанию.

Вместо этого он создает временный std::array с использованием агрегатной инициализации, затем перемещается (если доступен конструктор перемещения) или копируется в именованную переменную, когда функция возвращается. Поэтому нам нужен только конструктор перемещения или конструктор копирования.

Или так я думал...

Затем пришел этот фрагмент кода, который меня смутил:

struct foo {
    int x;
    foo(int x) : x(x) {}
    foo() = delete;
    foo(const foo&) = delete;
    foo& operator=(const foo&) = delete;
    foo(foo&&) = delete;
    foo& operator=(foo&&) = delete;
};

foo make_foo(int x) {
    return foo(x);
}

int main() {
    foo f = make_foo(1);
    foo g(make_foo(2));
}

Все пять специальных конструкторов/операторов специального элемента явно удалены, поэтому теперь я не смог бы построить свой объект из возвращаемого значения, правильно?

Неправильно.

К моему удивлению, это компилируется в gcc (с С++ 17)!

Почему это компилируется? Очевидно, чтобы вернуть foo из функции make_foo(), мы должны построить foo. Это означает, что в функции main() мы присваиваем или создаем foo из возвращаемого foo. Как это возможно?!

Ответ 1

Добро пожаловать в чудесный мир гарантированного копирования (новый для С++ 17. См. Также этот вопрос).

foo make_foo(int x) {
    return foo(x);
}

int main() {
    foo f = make_foo(1);
    foo g(make_foo(2));
}

Во всех этих случаях вы инициализируете foo из prvalue типа foo, поэтому мы просто игнорируем все промежуточные объекты и непосредственно инициализируем внешний объект из фактического инициализатора. Это в точности эквивалентно:

foo f(1);
foo g(2);

Мы даже не рассматриваем конструкторы перемещения здесь, поэтому факт, что они удалены, не имеет значения. Специфическим правилом является [dcl.init]/17.6.1 - только после этой точки мы рассматриваем конструкторы и выполняем разрешение перегрузки.

Ответ 2

Обратите внимание, что pre-С++ 17 (до гарантированного копирования), вы уже можете вернуть этот объект с помощью скопированных списков:

foo make_foo(int x) {
    return {x}; // Require non explicit foo(int).
                // Doesn't copy/move.
}

Но использование будет иным:

foo&& f = make_foo(1);
foo&& g(make_foo(2));