Удаленный конструктор по умолчанию. Объекты все еще могут быть созданы... иногда

Наивный, оптимистичный и, о-о... неправильный взгляд на унифицированный синтаксис инициализации 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 вперед.

Другие вопросы по тегам