Чому C ++ вимагає користувацького конструктора за замовчуванням для побудови за замовчуванням об’єкта const?


99

Стандарт C ++ (розділ 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;
}

2
Здається, рядок не потрібен у вашому прикладі (див. Ideone.com/qqiXR ), оскільки ви оголосили, але не визначили / ініціалізували a, але gcc-4.3.4 приймає його навіть тоді, коли ви це робите (див. Ideone.com/uHvFS )
Ray Toal

Приклад вище і декларує, і визначає a. Comeau видає помилку "const змінної" a "вимагає ініціалізатора - клас" A "не має явно оголошеного конструктора за замовчуванням", якщо рядок коментується.
Кару


4
Це зафіксовано на C ++ 11, ви можете написати const A a{}:)
Говард Ловатт

Відповіді:


10

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

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

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

Якщо програма вимагає ініціалізації за замовчуванням об’єкта типу const-кваліфікованого типу T, T повинен бути тип-клас const-default-конструюваний тип або його масив.

Це формулювання по суті означає, що очевидний код працює. Якщо ви ініціалізуєте всі ваші бази та члени, ви можете сказати 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;оскільки нова редакція все ще говорить "надано користувачем", а не "оголошено користувачем", і мені залишається чухати голову, чому комітет вирішив виключити з цієї ДР явно дефолтовані конструктори за замовчуванням, змусивши нас зробити наші класи нетривіальні, якщо ми хочемо скласти об'єкти з неініціалізованими членами.
Окталіст

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.

Зазначений користувачем конструктор - це один із способів зробити клас не-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 (або його масиву), об'єкт ініціалізується за замовчуванням; [...]

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

Під ініціалізацією за замовчуванням об'єкта типу T означає:
- якщо T - тип класу, який не є POD (п. 9), викликається конструктор за замовчуванням для T (ініціалізація неправильно формується, якщо T не має доступного конструктора за замовчуванням);

Він просто говорить про конструктор за замовчуванням T, незалежно від того, чи визначено його користувачем або створений компілятором, не має значення.

Якщо вам це зрозуміло, то зрозумійте, що говорить далі специфікація ((§ 8.5 / 9),

[...]; якщо об’єкт має const-тип, базовий тип класу повинен мати оголошений користувачем конструктор за замовчуванням.

Отже, з цього тексту випливає, що програма буде неправильно сформована, якщо об'єкт має тип 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заборонено.
Кару

1
Інший спосіб зробити клас не-POD - це надання йому члена даних, який не є ПОД (наприклад, моя структура Bу запитанні). Але наданий користувачем конструктор за замовчуванням все ж потрібен у такому випадку.
Кару

"Якщо програма вимагає ініціалізації за замовчуванням об'єкта типу, призначеного для const, T, T повинен бути типом класу з наданим користувачем конструктором за замовчуванням."
Кару

@Karu: Я це прочитав. Здається, у специфікації є інші уривки, що дозволяє constоб'єкту , який не є POD, ініціалізуватися, викликаючи конструктор за замовчуванням, згенерований компілятором.
Наваз

2
Здається, ваші посилання Ideone порушені, і було б чудово, якби цю відповідь можна було оновити до C ++ 11/14, оскільки § 8.5 взагалі не згадує POD.
Окталіст

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

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


Але додавши A(){}, помилка зникне, тож нічого не заважає. Правило не працює рекурсивно - X(){}для цього прикладу ніколи не потрібно.
Кару

2
Ну, принаймні, змушуючи програміста додати конструктор, він змушений хвилинку подумати над проблемою і, можливо, придумає нетривіальну
arne

@Karu я вирішив лише половину питання - це виправлено :)
Люк Дантон

4
@arne: Єдина проблема полягає в тому, що це неправильний програміст. Людина, яка намагається інстанціювати клас, може подумати про все, що він хоче, але вони не зможуть змінити клас. Автор класу подумав про членів, побачив, що всі вони розумно ініціалізовані неявним конструктором за замовчуванням, тому ніколи не додавали жодного.
Кару

3
Що я взяв з цієї частини стандарту, це "завжди оголошувати конструктор за замовчуванням для типів, що не мають POD, на випадок, коли хтось хоче зробити екземпляр const одного дня". Це здається трохи надмірним.
Кару

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
Ось моє запропоноване формулювання: "Якщо програма вимагає ініціалізації за замовчуванням об'єкта типу const-кваліфікованого типу T, T має бути клас не-POD." Це було б const POD x;незаконно так само, як const int x;і незаконно (що має сенс, оскільки це є непотрібним для POD), але має const NonPOD x;законний характер (що має сенс, оскільки у нього можуть бути суб'єкти, що містять корисні конструктори / деструктори, або мати корисний конструктор / деструктор) .
Кару

@Karu - Це формулювання може спрацювати. Я звик до стандартизації RFC, і тому відчуваю, що "T має бути" слід читати "T must be". Але так, це могло б спрацювати.
всезначний

@Karu - Що з Stru NonPod {int i; віртуальна пустота f () {}}? Немає сенсу робити const NonPod x; юридичний.
грузоватор

1
@gruzovator Чи було б більше сенсу, якби у вас був порожній оголошений користувачем конструктор за замовчуванням? Моя пропозиція просто намагається усунути безглузду вимогу стандарту; з або без нього існує ще нескінченно багато способів написання коду, що не має сенсу.
Кару

1
@Karu Я з вами згоден. Через це правило в стандарті існує багато класів, які повинні мати визначений користувачем порожній конструктор. Мені подобається поведінка gcc. Це дозволяє, наприклад, struct NonPod { std::string s; }; const NonPod x;І видає помилку, коли NonPod єstruct NonPod { int i; std::string s; }; const NonPod x;
грузоватор
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.