Класс со всеми автоматически сгенерированными конструкторами/операторами удаляется, все еще может быть возвращен из функции?
Недавно я наткнулся на этот ответ, в котором описывается, как инициализировать 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));