Почему C ++ требует, чтобы конструктор по умолчанию, предоставляемый пользователем, по умолчанию создавал константный объект?

99

Стандарт C ++ (раздел 8.5) гласит:

Если программа вызывает инициализацию по умолчанию объекта типа T с определением const, T должен быть типом класса с конструктором по умолчанию, предоставляемым пользователем.

Зачем? Я не могу придумать ни одной причины, по которой в этом случае требуется пользовательский конструктор.

struct B{
  B():x(42){}
  int doSomeStuff() const{return x;}
  int x;
};

struct A{
  A(){}//other than "because the standard says so", why is this line required?

  B b;//not required for this example, just to illustrate
      //how this situation isn't totally useless
};

int main(){
  const A a;
}
Кару
источник
2
Строка, похоже, не требуется в вашем примере (см. Ideone.com/qqiXR ), потому что вы объявили, но не определили / инициализировали a, но gcc-4.3.4 принимает ее, даже когда вы это делаете (см. Ideone.com/uHvFS )
Ray Toal
В приведенном выше примере объявляется и определяется a. Комо выдает ошибку «константная переменная« a »требует инициализатора - класс« A »не имеет явно объявленного конструктора по умолчанию», если строка закомментирована.
Karu
1
open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#253
Джонатан Уэйкли,
4
Это исправлено в C ++ 11, вы можете написать const A a{}:)
Говард Ловатт,

Ответы:

10

Это было сочтено дефектом (по отношению ко всем версиям стандарта) и устранено дефектом 253 основной рабочей группы (CWG) . Новая формулировка стандартных состояний в http://eel.is/c++draft/dcl.init#7

Тип класса T является константно-конструктивным по умолчанию, если инициализация по умолчанию T вызовет предоставленный пользователем конструктор T (не унаследованный от базового класса) или если

  • каждый прямой невариантный нестатический элемент данных M из T имеет инициализатор члена по умолчанию или, если M имеет тип класса X (или его массив), X является конструктивным по умолчанию const,
  • если T является объединением по крайней мере с одним нестатическим членом данных, ровно один вариантный член имеет инициализатор члена по умолчанию,
  • если T не является объединением, для каждого анонимного члена объединения с хотя бы одним нестатическим членом данных (если есть) ровно один нестатический член данных имеет инициализатор члена по умолчанию, и
  • каждый потенциально сконструированный базовый класс T является конструируемым по умолчанию.

Если программа вызывает инициализацию по умолчанию объекта константного типа T, T должен быть типом класса, конструируемым по умолчанию, или его массивом.

Эта формулировка по сути означает, что очевидный код работает. Если вы инициализируете все свои базы и элементы, вы можете сказать, A const a;независимо от того, как или если вы пишете какие-либо конструкторы.

struct A {
};
A const a;

gcc принимает это с 4.6.4. clang принял это с 3.9.0. Visual Studio также принимает это (по крайней мере, в 2017 году, не уверен, что раньше).

Дэвид Стоун
источник
3
Но это все еще запрещает, struct A { int n; A() = default; }; const A a;хотя разрешает, struct B { int n; B() {} }; const B b;потому что в новой формулировке по-прежнему говорится «предоставлено пользователем», а не «объявлено пользователем», и я остаюсь ломать голову, почему комитет решил исключить конструкторы по умолчанию с явным значением по умолчанию из этого DR, вынуждая нас сделать наши классы нетривиальны, если нам нужны константные объекты с неинициализированными членами.
Oktalist
1
Интересно, но есть еще крайний случай, с которым я столкнулся. С MyPODбудучи POD struct, static MyPOD x;- опираясь на нулевой инициализации (это то , что правый?) , Чтобы установить переменную члена (ов) соответственно - компилировать, но static const MyPOD x;не делает. Есть ли шанс, что это будет исправлено?
Джошуа Грин
66

Причина в том, что если у класса нет определяемого пользователем конструктора, то он может быть POD, а класс POD не инициализируется по умолчанию. Итак, если вы объявляете неинициализированный константный объект POD, какой в ​​этом смысл? Поэтому я думаю, что Стандарт применяет это правило, чтобы объект действительно мог быть полезным.

struct POD
{
  int i;
};

POD p1; //uninitialized - but don't worry we can assign some value later on!
p1.i = 10; //assign some value later on!

POD p2 = POD(); //initialized

const POD p3 = POD(); //initialized 

const POD p4; //uninitialized  - error - as we cannot change it later on!

Но если вы сделаете класс не-POD:

struct nonPOD_A
{
    nonPOD_A() {} //this makes non-POD
};

nonPOD_A a1; //initialized 
const nonPOD_A a2; //initialized 

Обратите внимание на разницу между POD и не-POD.

Пользовательский конструктор - это один из способов сделать класс не-POD. Есть несколько способов сделать это.

struct nonPOD_B
{
    virtual void f() {} //virtual function make it non-POD
};

nonPOD_B b1; //initialized 
const nonPOD_B b2; //initialized 

Обратите внимание, что nonPOD_B не определяет пользовательский конструктор. Скомпилируйте это. Он скомпилирует:

И прокомментируйте виртуальную функцию, тогда она выдаст ошибку, как и ожидалось:


Ну, я думаю, вы неправильно поняли отрывок. Сначала он говорит следующее (§8.5 / 9):

Если для объекта не указан инициализатор, и объект относится к типу ( возможно, cv-квалификационному) не-POD (или его массиву), объект должен быть инициализирован по умолчанию; [...]

Он говорит о не-POD-классе, возможно, о типе с квалификацией cv . То есть объект, не относящийся к POD, должен быть инициализирован по умолчанию, если не указан инициализатор. А что инициализировано по умолчанию ? Для не-POD спецификация говорит (§8.5 / 5),

Инициализация по умолчанию объекта типа T означает:
- если T не относится к типу класса POD (пункт 9), вызывается конструктор по умолчанию для T (и инициализация плохо сформирована, если T не имеет доступного конструктора по умолчанию);

Он просто говорит о конструкторе по умолчанию для T, независимо от того, не имеет значения его определяемый пользователем или созданный компилятором.

Если вы понимаете это, тогда поймите, что говорит спецификация дальше ((§8.5 / 9),

[...]; если объект имеет тип, определяемый константой, базовый тип класса должен иметь конструктор по умолчанию, объявленный пользователем.

Таким образом, этот текст подразумевает, что программа будет неправильно сформирована, если объект имеет тип POD, квалифицированный как const , и не указан инициализатор (поскольку POD не инициализируются по умолчанию):

POD p1; //uninitialized - can be useful - hence allowed
const POD p2; //uninitialized - never useful  - hence not allowed - error

Между прочим, он компилируется нормально , потому что он не POD и может быть инициализирован по умолчанию .

Наваз
источник
1
Я считаю, что ваш последний пример - это ошибка компиляции - у nonPOD_Bнего нет конструктора по умолчанию, предоставленного пользователем, поэтому строка const nonPOD_B b2не разрешена.
Karu
1
Другой способ сделать класс не-POD - предоставить ему член данных, который не является POD (например, моя структура Bв вопросе). Но в этом случае по-прежнему требуется конструктор по умолчанию, предоставляемый пользователем.
Karu
«Если программа вызывает инициализацию по умолчанию объекта константного типа T, T должен быть типом класса с предоставленным пользователем конструктором по умолчанию».
Karu
@Karu: Я читал это. Похоже, что в спецификации есть и другие отрывки, которые позволяют constинициализировать объект, не являющийся POD, путем вызова конструктора по умолчанию, созданного компилятором.
Nawaz
2
Ваши ссылки на ideone кажутся неработающими, и было бы здорово, если бы этот ответ можно было обновить до C ++ 11/14, потому что в §8.5 вообще не упоминается POD.
Oktalist
12

Чистое предположение с моей стороны, но учтите, что другие типы тоже имеют подобное ограничение:

int main()
{
    const int i; // invalid
}

Таким образом, это правило не только согласовано, но и (рекурсивно) предотвращает единичные const(под) объекты:

struct X {
    int j;
};
struct A {
    int i;
    X x;
}

int main()
{
    const A a; // a.i and a.x.j in unitialized states!
}

Что касается другой стороны вопроса (допуская это для типов с конструктором по умолчанию), я думаю, что идея состоит в том, что тип с конструктором по умолчанию, предоставляемым пользователем, должен всегда находиться в каком-то разумном состоянии после создания. Обратите внимание, что правила, как они есть, допускают следующее:

struct A {
    explicit
    A(int i): initialized(true), i(i) {} // valued constructor

    A(): initialized(false) {}

    bool initialized;
    int i;
};

const A a; // class invariant set up for the object
           // yet we didn't pay the cost of initializing a.i

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

Люк Дантон
источник
Но при добавлении A(){}ошибка исчезнет, ​​поэтому ничего не мешает. Правило не работает рекурсивно - X(){}в этом примере никогда не требуется.
Karu
2
Ну, по крайней мере, заставляя программиста добавить конструктор, он вынужден на минутку задуматься над проблемой и, возможно, придумать нетривиальную
задачу
@Karu Я ответил только на половину вопроса - исправил это :)
Люк Дантон
4
@arne: Проблема только в том, что это не тот программист. Человек, пытающийся создать экземпляр класса, может подумать обо всем, что он хочет, но он не сможет изменить класс. Автор класса подумал о членах, увидел, что все они были разумно инициализированы неявным конструктором по умолчанию, поэтому никогда не добавлял его.
Karu
3
Что я взял из этой части стандарта, так это «всегда объявлять конструктор по умолчанию для типов, отличных от POD, на случай, если кто-то захочет однажды создать константный экземпляр». Это кажется излишним.
Karu
3

Я смотрел выступление Тимура Думлера на встрече C ++ 2018 и наконец понял, почему стандарт требует здесь конструктор, предоставляемый пользователем, а не просто объявленный пользователем. Это связано с правилами инициализации значений.

Рассмотрим два класса: Aимеет конструктор, объявленный пользователем, и конструктор, Bпредоставленный пользователем :

struct A {
    int x;
    A() = default;
};
struct B {
    int x;
    B() {}
};

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

  • A a;инициализация по умолчанию: член int xне инициализирован.
  • B b;инициализация по умолчанию: член int xне инициализирован.
  • A a{};это значение инициализации: элемент int xимеет нулевой инициализируется .
  • B b{};инициализация значения: член int xне инициализирован.

Теперь посмотрим, что произойдет, когда мы добавим const:

  • const A a;это инициализация по умолчанию: это неправильно сформировано из-за правила, указанного в вопросе.
  • const B b;инициализация по умолчанию: член int xне инициализирован.
  • const A a{};это значение инициализации: элемент int xимеет нулевой инициализируется .
  • const B b{};инициализация значения: член int xне инициализирован.

Неинициализированный constскаляр (например, int xчлен) был бы бесполезен: запись в него неправильно сформирована (потому что это const), а чтение из него - UB (потому что он содержит неопределенное значение). Таким образом, это правило не позволяет вам создать такую ​​вещь, вынуждая вас либо добавить инициализатор, либо отказаться от опасного поведения, добавив предоставленный пользователем конструктор.

Я думаю, было бы неплохо иметь атрибут, [[uninitialized]]сообщающий компилятору, когда вы намеренно не инициализируете объект. Тогда нам не пришлось бы делать наш класс не конструктивным по умолчанию, чтобы обойти этот угловой случай. Этот атрибут фактически был предложен , но, как и все другие стандартные атрибуты, он не требует какого-либо нормативного поведения, являясь просто подсказкой для компилятора.

Окталист
источник
1

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

Можете ли вы предложить разумную новую формулировку правила, которое охватывает ваше дело, но при этом делает дела, которые должны быть незаконными, незаконными? Это меньше 5 или 6 абзацев? Легко и очевидно, как его применять в любой ситуации?

Я утверждаю, что придумать правило, позволяющее придать смысл созданному вами объявлению, действительно сложно, а еще сложнее убедиться, что это правило может применяться таким образом, чтобы это было понятно людям при чтении кода. Я бы предпочел несколько ограничительное правило, которое было бы правильным в большинстве случаев, чем очень тонкое и сложное правило, которое было трудно понять и применить.

Вопрос в том, есть ли веская причина, по которой правило должно быть более сложным? Есть ли код, который в противном случае было бы очень сложно написать или понять, который можно было бы написать намного проще, если бы правило было более сложным?

Всевозможный
источник
1
Вот предлагаемая мной формулировка: «Если программа вызывает инициализацию по умолчанию объекта типа T с определением const, T должен быть типом класса, отличным от POD». Это сделало бы const POD x;незаконным, так же как const int x;и незаконным (что имеет смысл, потому что это бесполезно для POD), но сделало бы const NonPOD x;законным (что имеет смысл, потому что у него могут быть подобъекты, содержащие полезные конструкторы / деструкторы, или сам полезный конструктор / деструктор) .
Karu
@Karu - Эта формулировка может сработать. Я привык к RFC standardsese, и поэтому считаю, что «T should be» должно читаться как «T must be». Но да, это могло сработать.
Omnifarious
@Karu - А как насчет struct NonPod {int i; виртуальная пустота f () {}}? Не имеет смысла делать const NonPod x; законный.
грузоватор
1
@gruzovator Было бы больше смысла, если бы у вас был пустой объявленный пользователем конструктор по умолчанию? Мое предложение всего лишь попытка удалить бессмысленное требование стандарта; с ним или без него, есть еще бесконечно много способов написать бессмысленный код.
Кару
1
@Karu Я согласен с тобой. Из-за этого правила в стандарте существует множество классов, которые должны иметь определяемый пользователем пустой конструктор. Мне нравится поведение gcc. struct NonPod { std::string s; }; const NonPod x;Допускает, например, и выдает ошибку, когда стоит NonPodstruct NonPod { int i; std::string s; }; const NonPod x;
грузоватор