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

Наивный, оптимистичный и о ... так неправильный взгляд на унифицированный синтаксис инициализации С ++ 11

Я думал, что, поскольку объекты пользовательского типа C ++ 11 должны быть созданы с новым синтаксисом {...} вместо старого синтаксиса (...) (за исключением конструктора, перегруженного для std::initializer_list и подобных параметров (например, std::vector: size ctor vs 1 elem init_list ctor)) .

Преимущества: отсутствие узких неявных преобразований, отсутствие проблем с самым неприятным синтаксическим анализом, согласованность (?). Я не видел проблем, так как думал, что они такие же (кроме приведенного примера).

Но это не так.

Рассказ о чистом безумии

{} вызывает конструктор по умолчанию.

... За исключением случаев:

  • конструктор по умолчанию удаляется и
  • другие конструкторы не определены.

Тогда похоже, что это скорее значение инициализирует объект? ... Даже если объект удалил конструктор по умолчанию, {} может создать объект. Разве это не превосходит всю цель удаленного конструктора?

... За исключением случаев:

  • объект имеет удаленный конструктор по умолчанию и
  • другие конструкторы определены.

Затем он терпит неудачу с call to deleted constructor.

... За исключением случаев:

  • у объекта есть удаленный конструктор и
  • никакой другой конструктор не определен и
  • по крайней мере, нестатический член данных.

Затем происходит сбой с отсутствующими инициализаторами полей.

Но тогда вы можете использовать {value} для создания объекта.

Хорошо, возможно, это то же самое, что и первое исключение (значение инициализации объекта)

... За исключением случаев:

  • у класса есть удаленный конструктор
  • и по крайней мере один член данных в классе инициализирован по умолчанию.

Тогда ни {}, ни {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.

Сценарий B

Удален конструктор по умолчанию, остальные конструкторы удалены

struct foo {
  foo() = delete;
  foo(int) = delete;
};

foo f{}; // OK

Сценарий C

Удален конструктор по умолчанию, определены другие конструкторы

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)`


person bolov    schedule 29.11.2015    source источник
comment
это stackoverflow.com/questions/23882409/ отвечает на половину вопроса. Самый важный, но все еще не отвечающий на вопрос, что происходит с инициализируемыми членами данных класса и конструкторами, не являющимися конструкторами по умолчанию.   -  person bolov    schedule 30.11.2015
comment
Извини, я поторопился. Здесь инициализация агрегата выполняется именно потому, что конструктор определен как удаленный (при его первом объявлении).   -  person Columbo    schedule 30.11.2015
comment
Это не единственный случай безумия в современном C ++. В течение многих лет я слышал, что C ++ глуп, поскольку static означает очень разные вещи в зависимости от контекста (на самом деле есть только два очень разных значения и в явно разных контекстах). Затем изобретается decltype с двумя тонко разными значениями с очень тонким разным использованием: identifier vs: (identifier)   -  person curiousguy    schedule 28.07.2018
comment
никаких узких неявных преобразований И хорошо ли запретить совершенно допустимое и полезное преобразование только в одном конкретном случае?   -  person curiousguy    schedule 28.07.2018
comment
@curiousguy Я не понимаю о чем ты   -  person bolov    schedule 28.07.2018
comment
@bolov Сужающее преобразование - это неясное, плохо определенное понятие (как и явное преобразование); и если вы хотите запретить какой-либо тип преобразования, единственный способ - сделать это везде. Запрещение чего-то, что вам не нравится, внутри одного синтаксиса не ортогонально и нелогично.   -  person curiousguy    schedule 28.07.2018
comment
похоже, исправлено в c ++ 20: godbolt.org/z/Majaf6nTb   -  person PiotrNycz    schedule 14.04.2021


Ответы (3)


При таком взгляде легко сказать, что в способе инициализации объекта царит полный хаос.

Большая разница заключается в типе foo: является ли это агрегатным типом или нет.

Это совокупность, если в ней есть:

  • нет конструкторов, предоставленных пользователем (удаленная функция или функция по умолчанию не считается предоставленной пользователем),
  • нет частных или защищенных нестатических элементов данных,
  • нет фигурных скобок или равных инициализаторов для нестатических элементов данных (начиная с c ++ 11 до (возвращено) c ++ 14)
  • нет базовых классов,
  • нет виртуальных функций-членов.

So:

  • в сценариях A B D E: foo является совокупным
  • в сценариях C: foo не является совокупным
  • scenario F:
    • in c++11 it is not an aggregate.
    • в С ++ 14 это агрегат.
    • g++ hasn't implemented this and still treats it as a non-aggregate even in C++14.
      • 4.9 doesn't implement this.
      • 5.2.0 делает
      • 5.2.1 ubuntu нет (возможно, регресс)

Эффекты инициализации списка объекта типа T следующие:

  • ...
  • Если T является агрегатным типом, выполняется агрегатная инициализация. Это заботится о сценариях A B D E (и F в C ++ 14)
  • Otherwise the constructors of T are considered in two phases:
    • All constructors that take 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, который помог исправить и улучшить этот пост.

person bolov    schedule 29.11.2015
comment
может кто-нибудь знакомый со стандартом, пожалуйста, дважды проверьте мои ответы, чтобы убедиться, что я не сделал ошибок. Я до сих пор не совсем понимаю сложные правила инициализации. Большое тебе спасибо. - person bolov; 30.11.2015
comment
нет фигурных скобок или равных инициализаторов для нестатических элементов данных (начиная с C ++ 11) - это было отменено в C ++ 14 - person M.M; 30.11.2015
comment
Вы пишете прямую инициализацию, когда на самом деле имеете в виду неагрегатную инициализацию. Термин прямая инициализация относится к T{a,b,c} (и другим ситуациям) независимо от того, является ли T агрегатом. Вся инициализация - это либо прямая инициализация, либо инициализация копирования (с дальнейшими подклассами доступных). - person M.M; 30.11.2015
comment
Я действительно следил за en.cppreference.com/w/cpp/language/list_initialization . Я знаю, что это не каноническая ссылка - person bolov; 30.11.2015
comment
@ M.M, так почему же gcc 4.9 и 5 рассматривают F как неагрегат в режиме C ++ 14? Это ошибка? - person bolov; 30.11.2015
comment
В частности, там, где вы написали Эффекты прямой инициализации (сценарий Fb):, вы имеете в виду неагрегатный. Foo f{}; - это прямая инициализация во всех случаях. - person M.M; 30.11.2015
comment
Сценарий F является неагрегированным в C ++ 11 и агрегированным в C ++ 14. g ++ 5.1 правильно реализует это; g ++ 4.9.2 не сделал - person M.M; 30.11.2015
comment
Я пробовал с gcc-5.2.1, и он рассматривает F как неагрегат (ошибка: использование удаленной функции) - person bolov; 30.11.2015
comment
C ++ 17 позволит агрегатам иметь общедоступные базы, поэтому вам нужно сделать dummy_t частную (или защищенную, но я не вижу смысла) базу, чтобы это работало. - person T.C.; 18.04.2016
comment
Недавно у меня была странная ошибка с gcc 4.9, когда замена удаленного конструктора по умолчанию на явно определенный пользователем приводила к исчезновению segfault. Мой пользователь определил одно утверждение при вызове. Я не изучал ассемблерный код, сгенерированный для этого случая, но предполагаю, что возникают проблемы с поддержкой C ++ 14 (которая в любом случае является неполной в версии 4.9.3). - person Bruce Adams; 08.06.2016
comment
Я бы расширил пример, который превращает ваш UDT в неагрегат, используя частное наследование. В конце концов, вы бы не хотели давать возможность выбирать перегруженные функции для этого фиктивного типа. - person Giel; 12.03.2018

Что вас беспокоит, так это совокупная инициализация.

Как вы говорите, у использования инициализации списка есть свои преимущества и недостатки. (Термин «унифицированная инициализация» не используется в стандарте C ++).

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


Агрегаты не создаются через конструктор. (Технически они действительно могут быть, но это хороший способ подумать об этом). Вместо этого при создании агрегата выделяется память, а затем каждый член инициализируется в порядке в соответствии с тем, что находится в инициализаторе списка.

Неагрегаты создаются с помощью конструкторов, и в этом случае члены инициализатора списка являются аргументами конструктора.

На самом деле в вышеизложенном есть недостаток дизайна: если у нас есть T t1; T t2{t1};, то намерение состоит в том, чтобы выполнить копирующее построение. Однако (до C ++ 14), если T является агрегатом, вместо этого выполняется агрегатная инициализация, и первый член t2 инициализируется с помощью t1.

Этот недостаток был исправлен в отчете о дефектах, который модифицировал C ++ 14. , так что с этого момента копирующая конструкция проверяется, прежде чем мы перейдем к агрегированной инициализации.


Определение aggregate из C ++ 14:

Агрегат - это массив или класс (раздел 9) без конструкторов, предоставленных пользователем (12.1), без частных или защищенных нестатических элементов данных (раздел 11), без базовых классов (раздел 10) и без виртуальных функций (10.3). ).

В C ++ 11 значение по умолчанию для нестатического члена означало, что класс не является агрегатом; однако это было изменено для C ++ 14. Предоставлено пользователем означает объявлено пользователем, но не = default или = delete.


Если вы хотите убедиться, что вызов вашего конструктора никогда случайно не выполняет агрегатную инициализацию, вам нужно использовать ( ), а не { }, и избегать MVP другими способами.

person M.M    schedule 29.11.2015
comment
ИМХО эта область стандарта нуждается в доработке. Было бы неплохо, если бы классы можно было формально пометить как aggregate или неагрегатные, и, возможно, также, если бы агрегат-конструктор был формализован. - person M.M; 30.11.2015
comment
Или, может быть, полностью избавиться от агрегированных классов, т.е. обрабатывать все классы в соответствии с неагрегированными правилами. - person M.M; 30.11.2015
comment
Я думаю, что отчет о дефекте был подан для C ++ 11, чтобы его можно было исправить в C ++ 14. - person 5gon12eder; 30.11.2015
comment
@ 5gon12eder см. CWG 1467. C ++ 14 был опубликован без исправления; а затем он был признан дефектом в ноябре 2014 года. Я не уверен, означает ли это, что он считается применимым к C ++ 11 или нет (возможно, спорно, поскольку официально C ++ 14 полностью заменяет C ++ 11 , хотя компиляторам придется решать, что делать в режиме -std=c++11) - person M.M; 30.11.2015
comment
Бьярн использовал этот термин, поэтому я выбрал его stroustrup.com/C++11FAQ .html # uniform-init В любом случае полезно знать - person bolov; 30.11.2015
comment
Если у нас есть T t2{t1};, то намерение состоит в том, чтобы выполнить копирующее построение. - Если T не std::vector<boost::any>, и в этом случае цель состоит в том, чтобы инициализировать t2 списком инициализаторов, единственным элементом которого является (копия) t1. Этому посвящена основная проблема 2137. , который может быть исправлен в C ++ 17, если нам повезет. (Это было нормально в C ++ 11, а затем сломано в C ++ 14.) Если вам буквально нужно иметь копирующую конструкцию, синтаксис для этого - auto t2 = t1;. - person Quuxplusone; 30.11.2015
comment
@Quuxplusone argh ... Этот обходной путь не всегда возможен, например если мы идем через идеальную переадресацию - person M.M; 30.11.2015
comment
Если вы пишете функцию, которая перенаправляет свои аргументы конструктору T, то лучший способ уменьшить путаницу пользователя - следовать той же практике, что и vector<T>::emplace (т.е. allocator<T>::construct) и make_shared<T>, то есть T t2(std::forward<Args>(args)...); с круглыми скобками вместо фигурных скобок. Если foo(args...) конструирует T через идеальную пересылку, этой конструкции лучше выполнить тот же код, что и T(args...), иначе пользователь очень запутается. - person Quuxplusone; 30.11.2015

Эти случаи инициализации агрегата для большинства противоречат интуиции и были предметом предложения 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 выше.

...

Предлагаемые изменения:

Измените пункт 1 [dcl.init.aggr] следующим образом:

Агрегат - это массив или класс (раздел 12) с

  • без явных, предоставленных пользователем, u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲ или унаследованных конструкторов (15.1),

  • нет частных или защищенных нестатических элементов данных (пункт 14),

  • нет виртуальных функций (13.3), и

  • нет виртуальных, частных или защищенных базовых классов (13.1).

Измените пункт 17 [dcl.init.aggr] следующим образом:

[Примечание: совокупный массив или совокупный класс может содержать элементы типа class >> с конструктором предоставленным пользователем 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]
Изменение: класс с конструкторами, объявленными пользователем, никогда не является агрегатом.
Обоснование < / strong>: удалить потенциально подверженную ошибкам агрегатную инициализацию, которая может применяться вне зависимости от объявленных конструкторов класса.
Влияние на исходную функцию: действительный код C ++ 2017, который агрегатно инициализирует тип с помощью объявленный пользователем конструктор может быть неправильно сформирован или иметь другую семантику в этом международном стандарте.

Далее следуют примеры, которые я опускаю.

Предложение было принято и объединено с C ++ 20, мы можем найти последний черновик здесь, который содержит эти изменения, и мы можем видеть изменения в [dcl.init.aggr] p1.1 и [dcl.init.aggr] p17 и Разница объявлений C ++ 17.

Так что это должно быть исправлено в C ++ 20 вперед.

person Shafik Yaghmour    schedule 27.07.2018
comment
похоже, это исправлено в c ++ 20: godbolt.org/z/Majaf6nTb - person PiotrNycz; 14.04.2021