Se eliminó el constructor predeterminado. Los objetos aún se pueden crear … a veces

La vista ingenua, optimista y oh … tan incorrecta de la syntax de inicialización uniforme de c ++ 11

Pensé que dado que los objetos de tipo C ++ 11 definidos por el usuario deberían construirse con la nueva syntax {...} lugar de la antigua (...) syntax (excepto para el constructor sobrecargado para std::initializer_list y parámetros similares (p. Ej. std::vector : size ctor vs 1 elem init_list ctor)).

Los beneficios son: no hay conversiones implícitas estrechas, no hay problema con el análisis más irritante, la consistencia (?). No vi ningún problema ya que pensé que son lo mismo (excepto el ejemplo dado).

Pero no lo son.

Una historia de pura locura

{} Llama al constructor predeterminado.

… Excepto cuando:

  • el constructor predeterminado es eliminado y
  • no hay otros constructores definidos.

Entonces parece que más bien valora inicializa el objeto? … Incluso si el objeto ha eliminado el constructor predeterminado, {} puede crear un objeto. ¿Esto no supera el propósito de un constructor eliminado?

…Excepto cuando:

  • el objeto tiene un constructor predeterminado eliminado y
  • otro constructor (es) definido (s)

Luego falla con una call to deleted constructor .

…Excepto cuando:

  • el objeto tiene un constructor eliminado y
  • ningún otro constructor definido y
  • al menos un miembro de datos no estático.

Luego falla con los inicializadores de campo faltantes.

Pero luego puedes usar {value} para construir el objeto.

Ok, tal vez esto es lo mismo que la primera excepción (valor init el objeto)

…Excepto cuando:

  • la clase tiene un constructor eliminado
  • y al menos un miembro de datos predeterminado en la clase inicializado.

Entonces ni {} ni {value} pueden crear un objeto.

Estoy seguro de haberme perdido algunos. La ironía es que se llama syntax de inicialización uniforme . Lo digo de nuevo: syntax de inicialización UNIFORME .

¿Qué es esta locura?

Escenario A

Constructor predeterminado eliminado:

 struct foo { foo() = delete; }; // All bellow OK (no errors, no warnings) foo f = foo{}; foo f = {}; foo f{}; // will use only this from now on. 

Escenario B

Constructor predeterminado eliminado, otros constructores eliminados

 struct foo { foo() = delete; foo(int) = delete; }; foo f{}; // OK 

Escenario C

Constructor predeterminado eliminado, otros constructores definidos

 struct foo { foo() = delete; foo(int) {}; }; foo f{}; // error call to deleted constructor 

Escenario D

Constructor predeterminado eliminado, sin otros constructores definidos, miembro de datos

 struct foo { int a; foo() = delete; }; foo f{}; // error use of deleted function foo::foo() foo f{3}; // OK 

Escenario E

Constructor predeterminado eliminado, constructor T eliminado, miembro de datos T

 struct foo { int a; foo() = delete; foo(int) = delete; }; foo f{}; // ERROR: missing initializer foo f{3}; // OK 

Escenario F

Constructor predeterminado eliminado, inicializadores de miembros de datos en clase

 struct foo { int a = 3; foo() = delete; }; /* Fa */ foo f{}; // ERROR: use of deleted function `foo::foo()` /* Fb */ foo f{3}; // ERROR: no matching function to call `foo::foo(init list)` 

Al ver las cosas de esta manera, es fácil decir que hay un caos total y completo en la forma en que se inicializa un objeto.

La gran diferencia proviene del tipo de foo : si es un tipo agregado o no.

Es un agregado si tiene:

  • no hay constructores proporcionados por el usuario (una función eliminada o predeterminada no cuenta como provista por el usuario),
  • ningún miembro de datos no estáticos privados o protegidos,
  • sin llaves de inicialización para miembros de datos no estáticos (desde c ++ 11 hasta (revertido) c ++ 14)
  • no hay clases base,
  • no hay funciones de miembro virtual.

Asi que:

  • en escenarios ABDE: foo es un agregado
  • en los escenarios C: foo no es un agregado
  • escenario F:
    • en c ++ 11 no es un agregado.
    • en c ++ 14 es un agregado.
    • g ++ no ha implementado esto y aún lo trata como un no agregado incluso en C ++ 14.
      • 4.9 no implementa esto.
      • 5.2.0 hace
      • 5.2.1 ubuntu no (tal vez una regresión)

Los efectos de la inicialización de lista de un objeto de tipo T son:

  • Si T es un tipo agregado, se realiza la inicialización agregada. Esto cuida los escenarios ABDE (y F en C ++ 14)
  • De lo contrario, los constructores de T se consideran en dos fases:
    • Todos los constructores que toman std :: initializer_list …
    • de lo contrario, todos los […] constructores de T participan en la resolución de sobrecarga […] Esto se ocupa de C (y F en C ++ 11)

:

Inicialización agregada de un objeto de tipo T (escenarios ABDE (F c ++ 14)):

  • Cada miembro de clase no estático, en orden de aparición en la definición de clase, es copiado-inicializado de la cláusula correspondiente de la lista de inicializadores. (referencia de matriz omitida)

TL; DR

Todas estas reglas todavía pueden parecer muy complicadas e inducir dolor de cabeza. Personalmente, me simplifico demasiado esto (si me pego un tiro en el pie, que así sea: supongo que pasaré 2 días en el hospital en lugar de tener un par de docenas de días de dolores de cabeza):

  • para un agregado, cada miembro de datos se inicializa a partir de los elementos del inicializador de listas
  • otro llama al constructor

¿Esto no supera el propósito de un constructor eliminado?

Bueno, no sé nada de eso, pero la solución es hacer que foo no sea un agregado. La forma más general que no agrega ninguna sobrecarga y no cambia la syntax utilizada del objeto es hacerlo heredar de una estructura vacía:

 struct dummy_t {}; struct foo : dummy_t { foo() = delete; }; foo f{}; // ERROR call to deleted constructor 

En algunas situaciones (supongo que no hay miembros no estáticos), un alternativo sería eliminar el destructor (esto hará que el objeto no sea instanciable en ningún contexto):

 struct foo { ~foo() = delete; }; foo f{}; // ERROR use of deleted function `foo::~foo()` 

Esta respuesta usa información recostackda de:

  • Inicialización de valor de C ++ 14 con constructor eliminado

  • ¿Qué son los agregados y POD y cómo / por qué son especiales?

  • Inicialización de lista

  • Inicialización agregada
  • Inicialización directa

Muchas gracias a @MM que ayudó a corregir y mejorar esta publicación.

Lo que te ensucia es la inicialización agregada .

Como dices, existen beneficios y desventajas al usar la inicialización de listas. (El término “inicialización uniforme” no es utilizado por el estándar de C ++).

Uno de los inconvenientes es que la inicialización de la lista se comporta de manera diferente para los agregados que para los no agregados. Además, la definición de agregado cambia ligeramente con cada Estándar.


Los agregados no se crean a través de un constructor. (Técnicamente podrían serlo, pero esta es una buena manera de pensarlo). En cambio, cuando se crea un agregado, se asigna memoria y luego cada miembro se inicializa en orden de acuerdo con lo que está en el inicializador de la lista.

Los no agregados se crean a través de constructores, y en ese caso los miembros del inicializador de la lista son argumentos de constructor.

De hecho, hay un defecto de diseño en lo anterior: si tenemos T t1; T t2{t1}; T t1; T t2{t1}; , entonces la intención es realizar una copia de construcción. Sin embargo, (antes de C ++ 14) si T es un agregado, en su lugar ocurre la inicialización agregada, y el primer miembro de t2 se inicializa con t1 .

Este error se corrigió en un informe de defectos que modificó C ++ 14, por lo tanto, a partir de ahora, se comprueba la construcción de copias antes de pasar a la inicialización agregada.


La definición de agregado de C ++ 14 es:

Un agregado es una matriz o una clase (Cláusula 9) sin constructores proporcionados por el usuario (12.1), sin miembros de datos no estáticos protegidos o privados (Cláusula 11), sin clases base (Cláusula 10) y sin funciones virtuales (10.3 )

En C ++ 11, un valor predeterminado para un miembro no estático significaba que una clase no era un agregado; sin embargo eso fue cambiado para C ++ 14. Proporcionado por el usuario significa declarado por el usuario, pero no = default o = delete .


Si desea asegurarse de que su llamada de constructor nunca realiza la inicialización agregada accidentalmente, debe usar ( ) lugar de { } y evitar los MVP de otras maneras.

Estos casos en torno a la inicialización agregada son contraintuitivos para la mayoría y fueron el tema de la propuesta p1008: Prohibir agregados con constructores declarados por el usuario que dice:

C ++ actualmente permite inicializar algunos tipos con constructores declarados por el usuario a través de la inicialización agregada, evitando esos constructores. El resultado es un código que es sorprendente, confuso y con errores. Este documento propone una solución que hace que la semántica de inicialización en C ++ sea más segura, más uniforme y más fácil de enseñar. También discutimos los cambios de última hora que introduce este parche

e introduce algunos ejemplos, que se superponen muy bien con los casos que presenta:

 struct X { X() = delete; }; int main() { X x1; // ill-formed - default c'tor is deleted X x2{}; // compiles! } 

Claramente, la intención del constructor eliminado es evitar que el usuario inicialice la clase. Sin embargo, contrariamente a la intuición, esto no funciona: el usuario aún puede inicializar X a través de la inicialización agregada porque esto pasa por alto completamente a los constructores. El autor podría incluso eliminar explícitamente todos los valores predeterminados, copiar y mover el constructor, y todavía no puede evitar que el código del cliente ejemplifique X a través de la inicialización agregada como se indicó anteriormente. La mayoría de los desarrolladores de C ++ están sorprendidos por el comportamiento actual cuando se muestra este código. El autor de la clase X podría considerar alternativamente hacer que el constructor predeterminado sea privado. Pero si a este constructor se le da una definición predeterminada, esto tampoco impide la inicialización agregada (y por lo tanto, la creación de instancias) de la clase:

 struct X { private: X() = default; }; int main() { X x1; // ill-formed - default c'tor is private X x2{}; // compiles! } 

Debido a las reglas actuales, la inicialización agregada nos permite “construir por defecto” una clase incluso si no es, de hecho, constructable por defecto:

  static_assert(!std::is_default_constructible_v); 

pasaría por ambas definiciones de X arriba.

Los cambios propuestos son:

Modifique el [dcl.init.aggr] párrafo 1 de la siguiente manera:

Un agregado es una matriz o una clase (Cláusula 12) con

  • ningún constructor proporcionado por el usuario, explícito , declarado por el usuario o heredado (15.1),

  • ningún miembro de datos no estáticos privados o protegidos (Cláusula 14),

  • sin funciones virtuales (13.3), y

  • no hay clases base virtuales, privadas o protegidas (13.1).

Modificar [dcl.init.aggr] párrafo 17 de la siguiente manera:

[Nota: una matriz agregada o una clase agregada puede contener elementos de un tipo clase >> con un proporcionado por el usuario constructor declarado por el usuario (15.1). La inicialización de >> estos objetos agregados se describe en 15.6.1. -Finalizar nota]

Agregue lo siguiente a [diff.cpp17] en el anexo C, sección C.5 C ++ e ISO C ++ 2017:

C.5.6 Cláusula 11: declaradores [diff.cpp17.dcl.decl]

Subcláusula afectada : [dcl.init.aggr]
Cambio : una clase que tiene constructores declarados por el usuario nunca es un agregado.
Justificación : elimine la inicialización agregada potencialmente propensa a errores que puede aplicarse a pesar de los constructores declarados de una clase.
Efecto en la característica original : el código válido de C ++ 2017 que agrega un tipo de inicializador con un constructor declarado por el usuario puede estar mal formado o tener una semántica diferente en esta norma internacional.

Seguido de ejemplos que omito.

La propuesta fue aceptada y fusionada en C ++ 20 , podemos encontrar el último borrador aquí que contiene estos cambios y podemos ver los cambios en [dcl.init.aggr] p1.1 y [dcl.init.aggr] p17 y C ++ 17 declaraciones diff .

Entonces esto debería arreglarse en C ++ 20 en adelante.

    Intereting Posts