Pourquoi C ++ nécessite-t-il un constructeur par défaut fourni par l'utilisateur pour construire par défaut un objet const?

99

Le standard C ++ (section 8.5) dit:

Si un programme appelle l'initialisation par défaut d'un objet d'un type qualifié const T, T doit être un type de classe avec un constructeur par défaut fourni par l'utilisateur.

Pourquoi? Je ne vois aucune raison pour laquelle un constructeur fourni par l'utilisateur est requis dans ce cas.

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;
}
Karu
la source
2
La ligne ne semble pas être requise dans votre exemple (voir ideone.com/qqiXR ) parce que vous avez déclaré mais n'avez pas défini / initialisé a, mais gcc-4.3.4 l'accepte même lorsque vous le faites (voir ideone.com/uHvFS )
Ray Toal le
L'exemple ci-dessus déclare et définit à la fois a. Comeau produit une erreur "variable const" a "nécessite un initialiseur - la classe" A "n'a pas de constructeur par défaut explicitement déclaré" si la ligne est commentée.
Karu le
4
Ceci est corrigé dans C ++ 11, vous pouvez écrire const A a{}:)
Howard Lovatt

Réponses:

10

Cela a été considéré comme un défaut (par rapport à toutes les versions de la norme) et a été résolu par le défaut 253 du groupe de travail central (CWG) . Le nouveau libellé des états standard dans http://eel.is/c++draft/dcl.init#7

Un type de classe T est const-default-constructible si l'initialisation par défaut de T invoquerait un constructeur de T fourni par l'utilisateur (non hérité d'une classe de base) ou si

  • chaque membre de données non statique direct non variant M de T a un initialiseur de membre par défaut ou, si M est de type de classe X (ou de son tableau), X est const-default-constructible,
  • si T est une union avec au moins un membre de données non statique, exactement un membre variant a un initialiseur de membre par défaut,
  • si T n'est pas une union, pour chaque membre d'union anonyme avec au moins un membre de données non statique (le cas échéant), exactement un membre de données non statique a un initialiseur de membre par défaut, et
  • chaque classe de base de T potentiellement construite est const-default-constructible.

Si un programme demande l'initialisation par défaut d'un objet d'un type qualifié const T, T doit être un type de classe constructible const-default-ou un tableau de celui-ci.

Cette formulation signifie essentiellement que le code évident fonctionne. Si vous initialisez toutes vos bases et tous vos membres, vous pouvez dire A const a;comment ou si vous épelez des constructeurs.

struct A {
};
A const a;

gcc accepte cela depuis la version 4.6.4. clang a accepté cela depuis 3.9.0. Visual Studio accepte également cela (au moins en 2017, je ne sais pas si plus tôt).

David Stone
la source
3
Mais cela interdit toujours struct A { int n; A() = default; }; const A a;tout en permettant struct B { int n; B() {} }; const B b;car le nouveau libellé dit toujours "fourni par l'utilisateur" et non "déclaré par l'utilisateur" et je me gratte la tête pourquoi le comité a choisi d'exclure de ce DR les constructeurs par défaut explicitement par défaut, nous obligeant à faire nos classes non triviales si nous voulons des objets const avec des membres non initialisés.
Oktalist
1
Intéressant, mais il y a toujours un cas critique que j'ai rencontré. En MyPODtant que POD struct, static MyPOD x;- en s'appuyant sur une initialisation à zéro (est-ce la bonne?) Pour définir la ou les variables membres de manière appropriée - compile, mais static const MyPOD x;ne le fait pas. Y a-t-il une chance que cela soit corrigé?
Joshua Green
66

La raison en est que si la classe n'a pas de constructeur défini par l'utilisateur, alors il peut s'agir de POD et la classe POD n'est pas initialisée par défaut. Donc si vous déclarez un objet const de POD qui n'est pas initialisé, à quoi cela sert-il? Je pense donc que la norme applique cette règle afin que l'objet puisse réellement être utile.

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!

Mais si vous faites de la classe un non-POD:

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

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

Notez la différence entre POD et non-POD.

Le constructeur défini par l'utilisateur est un moyen de rendre la classe non-POD. Vous pouvez le faire de plusieurs manières.

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

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

Remarquez que nonPOD_B ne définit pas de constructeur défini par l'utilisateur. Compilez-le. Il compilera:

Et commentez la fonction virtuelle, puis elle donne une erreur, comme prévu:


Eh bien, je pense que vous avez mal compris le passage. Il dit d'abord ceci (§8.5 / 9):

Si aucun initialiseur n'est spécifié pour un objet, et que l'objet est de type de classe non POD (éventuellement qualifié cv) (ou de son tableau), l'objet doit être initialisé par défaut; [...]

Il parle de classe non POD, éventuellement de type qualifié cv . Autrement dit, l'objet non POD doit être initialisé par défaut si aucun initialiseur n'est spécifié. Et qu'est-ce qui est initialisé par défaut ? Pour les non-POD, la spécification dit (§8.5 / 5),

Initialiser par défaut un objet de type T signifie:
- si T est un type de classe non POD (clause 9), le constructeur par défaut pour T est appelé (et l'initialisation est mal formée si T n'a pas de constructeur par défaut accessible);

Il parle simplement du constructeur par défaut de T, si son défini par l'utilisateur ou généré par le compilateur n'est pas pertinent.

Si vous êtes clair à ce sujet, alors comprenez ce que la spécification dit ensuite ((§8.5 / 9),

[...]; si l'objet est de type qualifié const, le type de classe sous-jacent doit avoir un constructeur par défaut déclaré par l'utilisateur.

Donc, ce texte implique que le programme sera mal formé si l'objet est de type POD qualifié par const et qu'aucun initialiseur n'est spécifié (car les POD ne sont pas initialisés par défaut):

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

À propos, cela compile très bien , car il n'est pas POD et peut être initialisé par défaut .

Nawaz
la source
1
Je crois que votre dernier exemple est une erreur de compilation - nonPOD_Bn'a pas de constructeur par défaut fourni par l'utilisateur, donc la ligne const nonPOD_B b2n'est pas autorisée.
Karu le
1
Une autre façon de faire de la classe un non-POD est de lui donner un membre de données qui n'est pas un POD (par exemple, ma structure Bdans la question). Mais le constructeur par défaut fourni par l'utilisateur est toujours requis dans ce cas.
Karu le
"Si un programme demande l'initialisation par défaut d'un objet d'un type T qualifié par const, T doit être un type de classe avec un constructeur par défaut fourni par l'utilisateur."
Karu le
@Karu: J'ai lu ça. Il semble qu'il y ait d'autres passages dans la spécification, qui permettent constaux objets non-POD d'être initialisés en appelant le constructeur par défaut généré par le compilateur.
Nawaz le
2
Vos liens ideone semblent être rompus, et ce serait bien si cette réponse pouvait être mise à jour vers C ++ 11/14 car le §8.5 ne mentionne pas du tout POD.
Oktalist
12

Pure spéculation de ma part, mais considérez que d'autres types ont également une restriction similaire:

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

Donc non seulement cette règle est cohérente, mais elle empêche également (récursivement) const (sous) :

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

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

En ce qui concerne l'autre côté de la question (l'autorisant pour les types avec un constructeur par défaut), je pense que l'idée est qu'un type avec un constructeur par défaut fourni par l'utilisateur est censé toujours être dans un état raisonnable après la construction. Notez que les règles telles qu'elles sont permettent ce qui suit:

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

Ensuite, nous pourrions peut-être formuler une règle du type «au moins un membre doit être correctement initialisé dans un constructeur par défaut fourni par l'utilisateur», mais c'est beaucoup trop de temps passé à essayer de se protéger contre Murphy. C ++ a tendance à faire confiance au programmeur sur certains points.

Luc Danton
la source
Mais en ajoutant A(){}, l'erreur disparaîtra, donc cela n'empêche rien. La règle ne fonctionne pas de manière récursive - X(){}n'est jamais nécessaire pour cet exemple.
Karu le
2
Eh bien, au moins en forçant le programmeur à ajouter un constructeur, il est obligé de réfléchir une minute au problème et peut-être en proposer un non trivial
arne
@Karu Je n'ai répondu qu'à la moitié de la question - corrigé ça :)
Luc Danton
4
@arne: Le seul problème est que c'est le mauvais programmeur. La personne essayant d'instancier la classe peut donner toute la réflexion qu'elle veut au sujet, mais elle ne pourra peut-être pas modifier la classe. L'auteur de la classe a pensé aux membres, a vu qu'ils étaient tous raisonnablement initialisés par le constructeur implicite par défaut, donc il n'en a jamais ajouté un.
Karu le
3
Ce que j'ai tiré de cette partie de la norme est "toujours toujours déclarer un constructeur par défaut pour les types non-POD, au cas où quelqu'un voudrait créer une instance const un jour". Cela semble un peu exagéré.
Karu le
3

Je regardais le discours de Timur Doumler à Meeting C ++ 2018 et j'ai finalement compris pourquoi la norme nécessite un constructeur fourni par l'utilisateur ici, pas simplement un constructeur déclaré par l'utilisateur. Cela concerne les règles d'initialisation des valeurs.

Considérez deux classes: Aa un constructeur déclaréB par l' utilisateur , a un constructeur fourni par l' utilisateur :

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

À première vue, vous pourriez penser que ces deux constructeurs se comporteront de la même manière. Mais voyez comment l'initialisation de valeur se comporte différemment, alors que seule l'initialisation par défaut se comporte de la même manière:

  • A a; est l'initialisation par défaut: le membre int x n'est pas initialisé.
  • B b; est l'initialisation par défaut: le membre int x n'est pas initialisé.
  • A a{};est l'initialisation de la valeur: le membre int xest initialisé à zéro .
  • B b{};est l'initialisation de la valeur: le membre int xn'est pas initialisé.

Maintenant, voyez ce qui se passe lorsque nous ajoutons const:

  • const A a;est l'initialisation par défaut: elle est mal formée en raison de la règle citée dans la question.
  • const B b;est l'initialisation par défaut: le membreint x n'est pas initialisé.
  • const A a{};est l'initialisation de la valeur: le membre int xest initialisé à zéro .
  • const B b{};est l'initialisation de la valeur: le membre int xn'est pas initialisé.

Un constscalaire non initialisé (par exemple le int xmembre) serait inutile: y écrire est mal formé (parce qu'il est const) et lire à partir de celui-ci est UB (car il contient une valeur indéterminée). Donc cette règle vous empêche de créer une telle chose, en vous obligeant soit à ajouter un initialiseur ou opt-in au comportement dangereux en ajoutant un constructeur fourni par l' utilisateur.

Je pense que ce serait bien d'avoir un attribut comme [[uninitialized]]dire au compilateur lorsque vous n'initialisez pas intentionnellement un objet. Ensuite, nous ne serions pas obligés de rendre notre classe non constructible par défaut pour contourner ce cas critique. Cet attribut a en fait été proposé , mais tout comme tous les autres attributs standard, il n'impose aucun comportement normatif, étant simplement un indice pour le compilateur.

Oktaliste
la source
1

Félicitations, vous avez inventé un cas dans lequel il n'y a pas besoin de constructeur défini par l'utilisateur pour que la constdéclaration sans initialiseur ait un sens.

Pouvez-vous maintenant proposer une reformulation raisonnable de la règle qui couvre votre cas tout en rendant les affaires qui devraient être illégales illégales? Est-ce moins de 5 ou 6 paragraphes? Est-il facile et évident de l'appliquer dans n'importe quelle situation?

Je suppose que trouver une règle qui permette à la déclaration que vous avez créée d'avoir un sens est vraiment difficile, et s'assurer que la règle peut être appliquée d'une manière qui a du sens pour les gens lors de la lecture du code est encore plus difficile. Je préférerais une règle quelque peu restrictive qui était la bonne chose à faire dans la plupart des cas à une règle très nuancée et complexe qui était difficile à comprendre et à appliquer.

La question est: y a-t-il une raison impérieuse pour laquelle la règle devrait être plus complexe? Y a-t-il un code qui serait autrement très difficile à écrire ou à comprendre qui pourrait être écrit beaucoup plus simplement si la règle est plus complexe?

Très varié
la source
1
Voici ma formulation suggérée: "Si un programme appelle l'initialisation par défaut d'un objet d'un type T qualifié par const, T sera un type de classe non POD.". Cela rendrait const POD x;illégal tout comme const int x;c'est illégal (ce qui a du sens, car cela est inutile pour un POD), mais const NonPOD x;légal (ce qui a du sens, car il pourrait avoir des sous-objets contenant des constructeurs / destructeurs utiles, ou avoir un constructeur / destructeur utile lui-même) .
Karu
@Karu - Ce libellé pourrait fonctionner. Je suis habitué à la norme RFC, et je pense donc que «T doit être» doit se lire «T doit être». Mais oui, cela pourrait fonctionner.
Omnifarious le
@Karu - Qu'en est-il de struct NonPod {int i; vide virtuel f () {}}? Cela n'a pas de sens de créer const NonPod x; légal.
gruzovator
1
@gruzovator Cela aurait-il plus de sens si vous aviez un constructeur par défaut déclaré par l'utilisateur vide? Ma suggestion tente simplement de supprimer une exigence inutile de la norme; avec ou sans lui, il existe encore une infinité de façons d'écrire du code qui n'a aucun sens.
Karu
1
@Karu je suis d'accord avec vous. En raison de cette règle standard, de nombreuses classes doivent avoir un constructeur vide défini par l'utilisateur . J'aime le comportement gcc. Il permet par exemple struct NonPod { std::string s; }; const NonPod x;et donne une erreur lorsque NonPod eststruct NonPod { int i; std::string s; }; const NonPod x;
gruzovator