Удаленный конструктор по умолчанию. Объекты все еще могут быть созданы... иногда
Наивный, оптимистичный и, о-о... неправильный взгляд на унифицированный синтаксис инициализации C++11
Я думал, что, поскольку C++11 объекты пользовательских типов должны быть построены с новым {...}
синтаксис вместо старого (...)
синтаксис (за исключением конструктора, перегруженного для std::initializer_list
и аналогичные параметры (например, std::vector
: размер ctor против 1 элемента init_list ctor)).
Преимущества: нет узких неявных преобразований, нет проблем с самым неприятным синтаксическим анализом, согласованность (?). Я не видел проблем, так как думал, что они одинаковы (кроме приведенного примера).
Но это не так.
Сказка о чистом безумии
{}
вызывает конструктор по умолчанию
... кроме случаев, когда:
- конструктор по умолчанию удаляется и
- другие конструкторы не определены.
Тогда похоже, что это скорее значение инициализирует объект?... Даже если объект удалил конструктор по умолчанию, {}
может создать объект. Разве это не превосходит всю цель удаленного конструктора?
... кроме случаев, когда:
- у объекта есть удаленный конструктор по умолчанию и
- другой конструктор (ы) определены.
Тогда это не с call to deleted constructor
,
... кроме случаев, когда:
- объект имеет удаленный конструктор и
- никакой другой конструктор не определен и
- по крайней мере нестатический член данных.
Тогда это терпит неудачу с отсутствующими инициализаторами поля.
Но тогда вы можете использовать {value}
построить объект.
Хорошо, возможно, это то же самое, что и первое исключение (значение init the object)
... кроме случаев, когда:
- у класса есть удаленный конструктор
- и по крайней мере один элемент данных в классе инициализирован по умолчанию.
Тогда ни {}
ни {value}
может создать объект.
Я уверен, что я пропустил несколько. Ирония в том, что это называется унифицированным синтаксисом инициализации. Я еще раз говорю: Синтаксис инициализации UNIFORM.
Что это за безумие?
Сценарий А
Удаленный конструктор по умолчанию:
struct foo {
foo() = delete;
};
// All bellow OK (no errors, no warnings)
foo f = foo{};
foo f = {};
foo f{}; // will use only this from now on.
Сценарий Б
Удаленный конструктор по умолчанию, другие конструкторы удалены
struct foo {
foo() = delete;
foo(int) = delete;
};
foo f{}; // OK
Сценарий С
Удаленный конструктор по умолчанию, другие конструкторы определены
struct foo {
foo() = delete;
foo(int) {};
};
foo f{}; // error call to deleted constructor
Сценарий D
Удаленный конструктор по умолчанию, другие конструкторы не определены, член данных
struct foo {
int a;
foo() = delete;
};
foo f{}; // error use of deleted function foo::foo()
foo f{3}; // OK
Сценарий E
Удаленный конструктор по умолчанию, удаленный конструктор T, член данных T
struct foo {
int a;
foo() = delete;
foo(int) = delete;
};
foo f{}; // ERROR: missing initializer
foo f{3}; // OK
Сценарий F
Удаленный конструктор по умолчанию, инициализаторы членов класса в классе
struct foo {
int a = 3;
foo() = delete;
};
/* Fa */ foo f{}; // ERROR: use of deleted function `foo::foo()`
/* Fb */ foo f{3}; // ERROR: no matching function to call `foo::foo(init list)`
3 ответа
При таком взгляде на вещи легко сказать, что в инициализации объекта есть полный и полный хаос.
Большая разница происходит от типа foo
: если это агрегатный тип или нет.
Это агрегат, если он имеет:
- нет пользовательских конструкторов (удаленная или дефолтная функция не считается предоставленной пользователем),
- нет частных или защищенных нестатических членов данных,
- нет инициализаторов скобок или равных для нестатических элементов данных (начиная с C++11 до (возвращено в) C++14)
- нет базовых классов,
- нет виртуальных функций-членов.
Так:
- в сценариях ABDE:
foo
это совокупность - в сценариях C:
foo
это не совокупность - сценарий F:
- в с ++ 11 это не агрегат.
- в с ++ 14 это агрегат.
- g ++ не реализовал это и все еще рассматривает это как неагрегированное даже в C++14.
4.9
не реализует это.5.2.0
делает5.2.1 ubuntu
нет (возможно, регрессия)
Эффекты инициализации списка объекта типа T:
- ...
- Если T является агрегатным типом, выполняется агрегатная инициализация. Это заботится о сценариях ABDE (и F в C++14)
- В противном случае конструкторы T рассматриваются в два этапа:
- Все конструкторы, которые принимают std::initializer_list ...
- в противном случае [...] все конструкторы T участвуют в разрешении перегрузки [...] Это заботится о C (и F в C++11)
- ...
:
Совокупная инициализация объекта типа T (сценарии A B D E (F C++14)):
- Каждый нестатический член класса, в порядке появления в определении класса, инициализируется копией из соответствующего предложения списка инициализаторов. (ссылка на массив опущена)
TL; DR
Все эти правила все еще могут казаться очень сложными и вызывать головную боль. Я лично слишком упрощаю это для себя (если я застрелюсь в ногу, пусть будет так: я думаю, я проведу 2 дня в больнице, а не пару десятков дней головных болей):
- для агрегата каждый элемент данных инициализируется из элементов инициализатора списка
- еще вызов конструктора
Разве это не превосходит всю цель удаленного конструктора?
Ну, я не знаю об этом, но решение состоит в том, чтобы сделать foo
не совокупность. Самая общая форма, которая не добавляет накладных расходов и не меняет используемый синтаксис объекта, состоит в том, чтобы заставить его наследовать от пустой структуры:
struct dummy_t {};
struct foo : dummy_t {
foo() = delete;
};
foo f{}; // ERROR call to deleted constructor
В некоторых ситуациях (я полагаю, нет нестатических элементов вообще) альтернативой может быть удаление деструктора (это сделает объект не подлежащим созданию в любом контексте):
struct foo {
~foo() = delete;
};
foo f{}; // ERROR use of deleted function `foo::~foo()`
Этот ответ использует информацию, полученную от:
Большое спасибо M.M, который помог исправить и улучшить этот пост.
Что вас беспокоит, так это общая инициализация.
Как вы говорите, у инициализации списка есть свои преимущества и недостатки. (Термин "равномерная инициализация" не используется стандартом C++).
Одним из недостатков является то, что инициализация списка ведет себя иначе для агрегатов, чем для неагрегатов. Кроме того, определение совокупности слегка меняется с каждым стандартом.
Агрегаты не создаются с помощью конструктора. (Технически они действительно могут быть, но это хороший способ думать об этом). Вместо этого при создании агрегата выделяется память, а затем каждый элемент инициализируется в порядке, соответствующем тому, что находится в инициализаторе списка.
Неагрегаты создаются с помощью конструкторов, и в этом случае члены инициализатора списка являются аргументами конструктора.
На самом деле есть недостаток дизайна в приведенном выше: если мы имеем T t1; T t2{t1};
, тогда цель состоит в том, чтобы выполнить копирование-конструирование. Однако (до C++14), если T
является агрегатом, то вместо этого происходит инициализация агрегата, и t2
Первый член инициализируется с t1
,
Этот недостаток был исправлен в отчете о дефектах, который изменил C++14, поэтому с этого момента проверяется конструкция копирования, прежде чем мы перейдем к инициализации агрегата.
Определение агрегата из C++ 14:
Агрегат - это массив или класс (раздел 9) без предоставленных пользователем конструкторов (12.1), без закрытых или защищенных нестатических элементов данных (пункт 11), без базовых классов (пункт 10) и без виртуальных функций (10.3).
В C++11 значение по умолчанию для нестатического члена означало, что класс не был агрегатом; однако это было изменено для C++14. Предоставленный пользователем означает, объявленный пользователем, но не = default
или же = delete
,
Если вы хотите убедиться, что ваш вызов конструктора никогда не выполняет случайную инициализацию агрегирования, тогда вам нужно использовать ( )
скорее, чем { }
и избегать MVP другими способами.
Эти случаи, связанные с инициализацией агрегатов, для большинства являются нелогичными и были предметом предложения p1008: Запретить агрегаты с помощью объявленных пользователем конструкторов, в которых говорится:
C++ в настоящее время позволяет инициализировать некоторые типы с объявленными пользователем конструкторами через агрегатную инициализацию, минуя эти конструкторы. В результате получается удивительный, запутанный и ошибочный код. В этой статье предлагается исправление, которое делает семантику инициализации в C++ более безопасной, унифицированной и удобной для обучения. Мы также обсуждаем критические изменения, которые вносит это исправление
и представляет некоторые примеры, которые хорошо совпадают с представленными вами случаями:
struct X { X() = delete; }; int main() { X x1; // ill-formed - default c’tor is deleted X x2{}; // compiles! }
Понятно, что цель удаленного конструктора - не дать пользователю инициализировать класс. Однако, вопреки интуиции, это не работает: пользователь все еще может инициализировать X с помощью агрегатной инициализации, потому что это полностью обходит конструкторы. Автор может даже явно удалить весь конструктор по умолчанию, копировать и переместить и все же не сможет помешать клиентскому коду создать экземпляр X через агрегатную инициализацию, как описано выше. Большинство разработчиков C++ удивляются текущему поведению, когда показывают этот код. Автор класса X может альтернативно рассмотреть возможность сделать конструктор по умолчанию закрытым. Но если этому конструктору дано определение по умолчанию, это опять-таки не предотвращает агрегатную инициализацию (и, следовательно, создание экземпляров) класса:
struct X { private: X() = default; }; int main() { X x1; // ill-formed - default c’tor is private X x2{}; // compiles! }
Из-за текущих правил, агрегатная инициализация позволяет нам "конструировать по умолчанию" класс, даже если он фактически не конструируется по умолчанию:
static_assert(!std::is_default_constructible_v<X>);
будет проходить для обоих определений X выше.
...
Предлагаемые изменения:
Изменить [dcl.init.aggr] пункт 1 следующим образом:
Агрегат - это массив или класс (раздел 12) с
нет пользовательских, явных, u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲ или унаследованных конструкторов (15.1),нет частных или защищенных нестатических элементов данных (пункт 14),
нет виртуальных функций (13.3), и
нет виртуальных, частных или защищенных базовых классов (13.1).
Изменить [dcl.init.aggr] пункт 17 следующим образом:
[Примечание: агрегатный массив или агрегатный класс могут содержать элементы класса >> типа с
предоставленный пользователемКонструктор u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲ (15.1). Инициализация >> этих агрегатных объектов описана в 15.6.1. —Конечная записка]Добавьте следующее в [diff.cpp17] в Приложении C, раздел C.5 C++ и ISO C++ 2017:
C.5.6 Пункт 11: деклараторы [diff.cpp17.dcl.decl]
Затрагиваемый подпункт: [dcl.init.aggr]
Изменение: класс, который имеет объявленные пользователем конструкторы, никогда не является агрегатом.
Обоснование: Удалите потенциально подверженную ошибкам агрегатную инициализацию, которая может применяться, не выдерживая заявленные конструкторы класса.
Эффект на исходную функцию: допустимый код C++ 2017, который агрегирует-инициализирует тип с помощью объявленного пользователем конструктора, может быть неверно сформирован или иметь другую семантику в этом международном стандарте.
Далее следуют примеры, которые я опускаю.
Предложение было принято и объединено в C++ 20, здесь мы можем найти последний черновик, в котором содержатся эти изменения, и увидеть изменения в [dcl.init.aggr]p1.1 и [dcl.init.aggr]p17 и C++17 объявлений diff.
Так что это должно быть исправлено в C++ 20 вперед.