¿Por qué C ++ requiere un constructor predeterminado proporcionado por el usuario para construir un objeto constante?

99

El estándar C ++ (sección 8.5) dice:

Si un programa solicita la inicialización predeterminada de un objeto de tipo T calificado const, T será un tipo de clase con un constructor predeterminado proporcionado por el usuario.

¿Por qué? No puedo pensar en ninguna razón por la que se requiera un constructor proporcionado por el usuario en este caso.

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
fuente
2
La línea no parece ser necesaria en su ejemplo (consulte ideone.com/qqiXR ) porque declaró pero no definió / inicializó a, pero gcc-4.3.4 la acepta incluso cuando lo haga (consulte ideone.com/uHvFS )
Ray Toal
El ejemplo anterior declara y define a. Comeau produce un error "const variable" a "requiere un inicializador - la clase" A "no tiene un constructor predeterminado declarado explícitamente" si la línea está comentada.
Karu
4
Esto está arreglado en C ++ 11, puede escribir const A a{}:)
Howard Lovatt

Respuestas:

10

Esto se consideró un defecto (contra todas las versiones del estándar) y fue resuelto por el Defecto 253 del Grupo de Trabajo Central (CWG) . La nueva redacción de los estados estándar en http://eel.is/c++draft/dcl.init#7

Un tipo de clase T es const-default-constructible si la inicialización predeterminada de T invocaría un constructor de T proporcionado por el usuario (no heredado de una clase base) o si

  • cada miembro de datos no estático directo no variante M de T tiene un inicializador de miembro predeterminado o, si M es del tipo de clase X (o matriz del mismo), X es const-default-constructible,
  • si T es una unión con al menos un miembro de datos no estático, exactamente un miembro variante tiene un inicializador de miembro predeterminado,
  • si T no es una unión, para cada miembro de la unión anónimo con al menos un miembro de datos no estáticos (si lo hay), exactamente un miembro de datos no estáticos tiene un inicializador de miembro predeterminado, y
  • cada clase base potencialmente construida de T es const-default-constructible.

Si un programa pide la inicialización por defecto de un objeto de un tipo T calificado const, T será un tipo de clase const-default-construible o una matriz del mismo.

Esta redacción esencialmente significa que el código obvio funciona. Si inicializa todas sus bases y miembros, puede decir A const a;independientemente de cómo o si escribe los constructores.

struct A {
};
A const a;

gcc ha aceptado esto desde 4.6.4. clang ha aceptado esto desde 3.9.0. Visual Studio también acepta esto (al menos en 2017, no estoy seguro si será antes).

David Stone
fuente
3
Pero esto todavía prohíbe struct A { int n; A() = default; }; const A a;mientras se permite struct B { int n; B() {} }; const B b;porque la nueva redacción aún dice "proporcionado por el usuario" no "declarado por el usuario" y me quedo rascándome la cabeza por qué el comité eligió excluir los constructores predeterminados explícitamente predeterminados de este DR, lo que nos obliga a hacer nuestras clases no son triviales si queremos objetos constantes con miembros no inicializados.
Oktalist
1
Interesante, pero todavía hay un caso límite con el que me he encontrado. Al MyPODser un POD struct, static MyPOD x;- confiando en la inicialización cero (¿es la correcta?) Para establecer la (s) variable (s) miembro de manera apropiada - compila, pero static const MyPOD x;no lo hace. ¿Existe alguna posibilidad de que se solucione?
Joshua Green
66

La razón es que si la clase no tiene un constructor definido por el usuario, entonces puede ser POD y la clase POD no se inicializa por defecto. Entonces, si declaras un objeto constante de POD que no está inicializado, ¿para qué sirve? Así que creo que el Estándar hace cumplir esta regla para que el objeto pueda ser realmente útil.

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!

Pero si hace que la clase no sea POD:

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

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

Tenga en cuenta la diferencia entre POD y no POD.

El constructor definido por el usuario es una forma de hacer que la clase no sea POD. Hay varias formas de hacerlo.

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

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

Observe que nonPOD_B no define un constructor definido por el usuario. Compílelo. Compilará:

Y comenta la función virtual, luego da error, como se esperaba:


Bueno, creo que no entendiste el pasaje. Primero dice esto (§8.5 / 9):

Si no se especifica ningún inicializador para un objeto, y el objeto es de tipo de clase no POD (posiblemente calificado por cv) (o matriz del mismo), el objeto se inicializará por defecto; [...]

Habla de una clase no POD posiblemente de tipo calificado para cv . Es decir, el objeto que no es POD se inicializará por defecto si no se especifica ningún inicializador. ¿Y qué se inicializa por defecto ? Para no POD, la especificación dice (§8.5 / 5),

Inicializar por defecto un objeto de tipo T significa:
- si T es un tipo de clase que no es POD (cláusula 9), se llama al constructor por defecto para T (y la inicialización está mal formada si T no tiene un constructor por defecto accesible);

Simplemente habla del constructor predeterminado de T, ya sea que sea definido por el usuario o generado por el compilador es irrelevante.

Si tiene claro esto, entienda lo que dice la siguiente especificación ((§8.5 / 9),

[...]; si el objeto es de tipo calificado const, el tipo de clase subyacente tendrá un constructor predeterminado declarado por el usuario.

Entonces, este texto implica que el programa estará mal formado si el objeto es de tipo POD calificado const , y no hay un inicializador especificado (porque los POD no están inicializados por defecto):

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

Por cierto, esto se compila bien , porque no es POD y puede inicializarse por defecto .

Nawaz
fuente
1
Creo que su último ejemplo es un error de compilación: nonPOD_Bno tiene un constructor predeterminado proporcionado por el usuario, por lo que la línea const nonPOD_B b2no está permitida.
Karu
1
Otra forma de hacer que la clase no sea POD es dándole un miembro de datos que no sea un POD (por ejemplo, mi estructura Ben la pregunta). Pero el constructor predeterminado proporcionado por el usuario sigue siendo necesario en ese caso.
Karu
"Si un programa solicita la inicialización predeterminada de un objeto de un tipo T calificado const, T será un tipo de clase con un constructor predeterminado proporcionado por el usuario".
Karu
@Karu: Lo he leído. Parece que hay otros pasajes en la especificación, que permiten que el constobjeto que no es POD se inicialice llamando al constructor predeterminado generado por el compilador.
Nawaz
2
Sus enlaces de ideone parecen estar rotos, y sería genial si esta respuesta pudiera actualizarse a C ++ 11/14 porque §8.5 no menciona POD en absoluto.
Oktalist
12

Pura especulación de mi parte, pero considere que otros tipos también tienen una restricción similar:

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

Entonces, esta regla no solo es consistente, sino que también (recursivamente) previene const(sub) objetos unitarios :

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 cuanto al otro lado de la pregunta (permitiéndolo para tipos con un constructor predeterminado), creo que la idea es que se supone que un tipo con un constructor predeterminado proporcionado por el usuario siempre debe estar en algún estado sensible después de la construcción. Tenga en cuenta que las reglas, tal como están, permiten lo siguiente:

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

Entonces tal vez podríamos formular una regla como 'al menos un miembro debe inicializarse con sensatez en un constructor predeterminado proporcionado por el usuario', pero eso es demasiado tiempo para tratar de protegerse contra Murphy. C ++ tiende a confiar en el programador en ciertos puntos.

Luc Danton
fuente
Pero al agregar A(){}, el error desaparecerá, por lo que no evitará nada. La regla no funciona de forma recursiva, X(){}nunca es necesaria para ese ejemplo.
Karu
2
Bueno, al menos al obligar al programador a agregar un constructor, se ve obligado a pensar un minuto en el problema y tal vez se le
ocurra
@Karu Solo abordé la mitad de la pregunta, solucioné eso :)
Luc Danton
4
@arne: El único problema es que es el programador equivocado. La persona que intenta crear una instancia de la clase puede pensar todo lo que quiera sobre el asunto, pero es posible que no pueda modificar la clase. El autor de la clase pensó en los miembros, vio que todos fueron inicializados con sensatez por el constructor implícito predeterminado, por lo que nunca agregó uno.
Karu
3
Lo que he tomado de esta parte del estándar es "siempre declarar siempre un constructor predeterminado para los tipos que no son POD, en caso de que alguien quiera hacer una instancia constante algún día". Eso parece un poco exagerado.
Karu
3

Estaba viendo la charla de Timur Doumler en Meeting C ++ 2018 y finalmente me di cuenta de por qué el estándar requiere un constructor proporcionado por el usuario aquí, no simplemente uno declarado por el usuario. Tiene que ver con las reglas para la inicialización de valores.

Considere dos clases: Atiene un constructor declarado por el usuario , Btiene un constructor proporcionado por el usuario :

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

A primera vista, podría pensar que estos dos constructores se comportarán igual. Pero vea cómo la inicialización de valores se comporta de manera diferente, mientras que solo la inicialización predeterminada se comporta de la misma manera:

  • A a;es la inicialización predeterminada: el miembro int xno está inicializado.
  • B b;es la inicialización predeterminada: el miembro int xno está inicializado.
  • A a{};es la inicialización del valor: el miembro int xse inicializa a cero .
  • B b{};es la inicialización del valor: el miembro int xno está inicializado.

Ahora mira lo que sucede cuando agregamos const:

  • const A a;es la inicialización predeterminada: está mal formada debido a la regla citada en la pregunta.
  • const B b;es la inicialización predeterminada: el miembro int xno está inicializado.
  • const A a{};es la inicialización del valor: el miembro int xse inicializa a cero .
  • const B b{};es la inicialización del valor: el miembro int xno está inicializado.

Un constescalar no inicializado (por ejemplo, el int xmiembro) sería inútil: escribir en él está mal formado (porque lo está const) y leer desde él es UB (porque tiene un valor indeterminado). Así que esta regla impide crear tal cosa, por lo que obligó a bien agregar un inicializador o opt-in para el comportamiento peligroso mediante la adición de un constructor proporcionado por el usuario.

Creo que sería bueno tener un atributo como [[uninitialized]]para decirle al compilador cuando intencionalmente no está inicializando un objeto. Entonces no estaríamos obligados a hacer que nuestra clase no sea trivialmente constructible por defecto para sortear este caso de esquina. Este atributo se ha propuesto en realidad , pero al igual que todos los demás atributos estándar, no exige ningún comportamiento normativo, siendo simplemente una pista para el compilador.

Oktalista
fuente
1

Felicitaciones, ha inventado un caso en el que no es necesario que haya ningún constructor definido por el usuario para que la constdeclaración sin inicializador tenga sentido.

Ahora, ¿puede pensar en una nueva redacción razonable de la regla que cubre su caso pero que aún hace que los casos que deberían ser ilegales sean ilegales? ¿Tiene menos de 5 o 6 párrafos? ¿Es fácil y obvio cómo se debe aplicar en cualquier situación?

Propongo que crear una regla que permita que la declaración que creó tenga sentido es realmente difícil, y asegurarse de que la regla se pueda aplicar de una manera que tenga sentido para las personas cuando leer el código es aún más difícil. Preferiría una regla algo restrictiva que fuera lo correcto en la mayoría de los casos a una regla compleja y con muchos matices que era difícil de entender y aplicar.

La pregunta es, ¿hay alguna razón de peso para que la regla sea más compleja? ¿Existe algún código que de otro modo sería muy difícil de escribir o entender que se pueda escribir de forma mucho más sencilla si la regla es más compleja?

De todo género
fuente
1
Aquí está mi redacción sugerida: "Si un programa requiere la inicialización predeterminada de un objeto de un tipo T calificado const, T será un tipo de clase no POD". Esto haría const POD x;ilegal al igual const int x;que ilegal (lo cual tiene sentido, porque esto es inútil para un POD), pero const NonPOD x;legal (lo cual tiene sentido, porque podría tener subobjetos que contengan constructores / destructores útiles, o tener un constructor / destructor útil en sí mismo) .
Karu
@Karu - Esa redacción podría funcionar. Estoy acostumbrado a las normas RFC, por lo que creo que 'T debe ser' debería leer 'T debe ser'. Pero sí, eso podría funcionar.
Omnifarious
@Karu - ¿Qué pasa con struct NonPod {int i; virtual vacío f () {}}? No tiene sentido hacer const NonPod x; legal.
gruzovator
1
@gruzovator ¿Tendría más sentido si tuviera un constructor predeterminado vacío declarado por el usuario? Mi sugerencia solo intenta eliminar un requisito inútil de la norma; con o sin él, todavía hay infinitas formas de escribir código que no tiene sentido.
Karu
1
@Karu Estoy de acuerdo contigo. Debido a esta regla en estándar, hay muchas clases que deben tener un constructor vacío definido por el usuario . Me gusta el comportamiento de gcc. Permite, por ejemplo, struct NonPod { std::string s; }; const NonPod x;y da un error cuando NonPod esstruct NonPod { int i; std::string s; }; const NonPod x;
gruzovator