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

В стандарте С++ (раздел 8.5) говорится:

Если программа запрашивает инициализацию по умолчанию объекта типа const, T, 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;
}

Ответ 1

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

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

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

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

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

struct A {
};
A const a;

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

Ответ 2

Причина в том, что если класс не имеет определяемого пользователем конструктора, то он может быть POD, а класс POD по умолчанию не инициализирован. Итак, если вы объявляете const-объект 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 

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

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


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

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

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

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

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

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

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

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

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

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

Ответ 3

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

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

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

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

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

Ответ 4

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

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

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

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

Ответ 5

Я наблюдал за выступлением Тимура Думлера на совещании 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]] чтобы сообщить компилятору, когда вы намеренно не инициализируете объект. Тогда мы не будем вынуждены делать наш класс нетривиально конструируемым по умолчанию, чтобы обойти этот угловой случай.