¿Es posible evitar la omisión de miembros de inicialización agregados?

43

Tengo una estructura con muchos miembros del mismo tipo, como este

struct VariablePointers {
   VariablePtr active;
   VariablePtr wasactive;
   VariablePtr filename;
};

El problema es que si olvido inicializar uno de los miembros de la estructura (por ejemplo wasactive), así:

VariablePointers{activePtr, filename}

El compilador no se quejará de ello, pero tendré un objeto que está parcialmente inicializado. ¿Cómo puedo evitar este tipo de error? Podría agregar un constructor, pero duplicaría la lista de variables dos veces, ¡así que tengo que escribir todo esto tres veces!

Agregue también las respuestas de C ++ 11 , si hay una solución para C ++ 11 (actualmente estoy restringido a esa versión). Sin embargo, los estándares de idiomas más recientes también son bienvenidos.

Johannes Schaub - litb
fuente
66
Escribir un constructor no suena tan terrible. A menos que tenga demasiados miembros, en cuyo caso, tal vez sea necesario refactorizar.
Gonen I
1
@Someprogrammerdude Creo que quiere decir que el error es que puede omitir accidentalmente un valor de inicialización
Gonen I
2
@theWiseBro si sabe cómo ayuda array / vector, debe publicar una respuesta. No es tan obvio, no lo veo
idclev 463035818
2
@Someprogrammerdude Pero, ¿es incluso una advertencia? No puedo verlo con VS2019.
acraig5075
8
Hay una -Wmissing-field-initializersbandera de compilación.
Ron

Respuestas:

42

Aquí hay un truco que desencadena un error de vinculador si falta un inicializador requerido:

struct init_required_t {
    template <class T>
    operator T() const; // Left undefined
} static const init_required;

Uso:

struct Foo {
    int bar = init_required;
};

int main() {
    Foo f;
}

Salir:

/tmp/ccxwN7Pn.o: In function `Foo::Foo()':
prog.cc:(.text._ZN3FooC2Ev[_ZN3FooC5Ev]+0x12): undefined reference to `init_required_t::operator int<int>() const'
collect2: error: ld returned 1 exit status

Advertencias:

  • Antes de C ++ 14, esto evita Fooser un agregado en absoluto.
  • Esto depende técnicamente de un comportamiento indefinido (violación de ODR), pero debería funcionar en cualquier plataforma sensata.
Quentin
fuente
Puede eliminar el operador de conversión y luego es un error del compilador.
jrok
@jrok sí, pero es uno tan pronto como Foose declara, incluso si nunca llama al operador.
Quentin
2
@jrok Pero luego no se compila incluso si se proporciona la inicialización. Godbolt.org/z/yHZNq_ Addendum: Para MSVC funciona como usted describió: godbolt.org/z/uQSvDa ¿Es esto un error?
n314159
Por supuesto, tonto de mí.
jrok
66
Desafortunadamente, este truco no funciona con C ++ 11, ya que se convertirá en no agregado entonces :( Eliminé la etiqueta C ++ 11, por lo que su respuesta también es viable (por favor, no la elimine), pero una solución C ++ 11 sigue siendo preferida, si es posible
Johannes Schaub - litb
22

Para clang y gcc puede compilar con -Werror=missing-field-initializerseso convierte la advertencia en los inicializadores de campo faltantes en un error. rayo

Editar: para MSVC, parece que no se emite ninguna advertencia incluso a nivel /Wall, por lo que no creo que sea posible advertir sobre los inicializadores faltantes con este compilador. rayo

n314159
fuente
7

No es una solución elegante y práctica, supongo ... pero debería funcionar también con C ++ 11 y dar un error en tiempo de compilación (no tiempo de enlace).

La idea es agregar en su estructura un miembro adicional, en la última posición, de un tipo sin inicialización predeterminada (y que no puede inicializarse con un valor de tipo VariablePtr(o lo que sea el tipo de valores anteriores)

Por ejemplo

struct bar
 {
   bar () = delete;

   template <typename T> 
   bar (T const &) = delete;

   bar (int) 
    { }
 };

struct foo
 {
   char a;
   char b;
   char c;

   bar sentinel;
 };

De esta forma, se ve obligado a agregar todos los elementos en su lista de inicialización agregada, incluido el valor para inicializar explícitamente el último valor (un entero para sentinel, en el ejemplo) o se obtiene un error de "llamada al constructor eliminado de 'barra'".

Entonces

foo f1 {'a', 'b', 'c', 1};

compilar y

foo f2 {'a', 'b'};  // ERROR

no lo hace

Lamentablemente también

foo f3 {'a', 'b', 'c'};  // ERROR

no compila

- EDITAR -

Como señaló MSalters (gracias) hay un defecto (otro defecto) en mi ejemplo original: un barvalor podría inicializarse con un charvalor (que es convertible a int), por lo que funciona la siguiente inicialización

foo f4 {'a', 'b', 'c', 'd'};

y esto puede ser muy confuso.

Para evitar este problema, he agregado el siguiente constructor de plantillas eliminado

 template <typename T> 
 bar (T const &) = delete;

entonces la f4declaración anterior da un error de compilación porque el dvalor es interceptado por el constructor de plantillas que se elimina

max66
fuente
Gracias, esto es bueno! No es perfecto como mencionaste, y también hace que foo f;no se compile, pero tal vez sea más una característica que un defecto con este truco. Aceptará si no hay mejor propuesta que esta.
Johannes Schaub - litb
1
Haría que el constructor de la barra acepte un miembro de clase anidado constante llamado algo así como init_list_end para facilitar la lectura
Gonen I
@GonenI: para facilitar la lectura, puede aceptar enumy nombrar init_list_end(o simplemente list_end) un valor de eso enum; pero la legibilidad agrega mucha mecanografía, por lo tanto, dado que el valor adicional es el punto débil de esta respuesta, no sé si es una buena idea.
max66
Tal vez agregue algo como constexpr static int eol = 0;en el encabezado de bar. test{a, b, c, eol}me parece bastante legible.
n314159
@ n314159 - bueno ... conviértete bar::eol; es casi como pasar un enumvalor; pero no creo que sea importante: el núcleo de la respuesta es "agregue en su estructura un miembro adicional, en la última posición, de un tipo sin inicialización predeterminada"; la barparte es solo un ejemplo trivial para mostrar que la solución funciona; el "tipo exacto sin inicialización predeterminada" debe depender de las circunstancias (en mi humilde opinión).
max66
4

Para CppCoreCheck, existe una regla para verificar exactamente eso, si todos los miembros se han inicializado y se puede convertir de advertencia en un error, que por lo general es de todo el programa.

Actualizar:

La regla que desea verificar es parte de typesafety Type.6:

Tipo.6: siempre inicializar una variable miembro: siempre inicializar, posiblemente utilizando constructores predeterminados o inicializadores de miembros predeterminados.

Darune
fuente
2

La forma más sencilla es no dar al tipo de miembros un constructor sin argumentos:

struct B
{
    B(int x) {}
};
struct A
{
    B a;
    B b;
    B c;
};

int main() {

        // A a1{ 1, 2 }; // will not compile 
        A a1{ 1, 2, 3 }; // will compile 

Otra opción: si sus miembros son constantes, debe inicializarlos a todos:

struct A {    const int& x;    const int& y;    const int& z; };

int main() {

//A a1{ 1,2 };  // will not compile 
A a2{ 1,2, 3 }; // compiles OK

Si puede vivir con un miembro y un miembro ficticios, puede combinarlo con la idea de @ max66 de un centinela.

struct end_of_init_list {};

struct A {
    int x;
    int y;
    int z;
    const end_of_init_list& dummy;
};

    int main() {

    //A a1{ 1,2 };  // will not compile
    //A a2{ 1,2, 3 }; // will not compile
    A a3{ 1,2, 3,end_of_init_list() }; // will compile

De cppreference https://en.cppreference.com/w/cpp/language/aggregate_initialization

Si el número de cláusulas de inicializador es menor que el número de miembros o la lista de inicializadores está completamente vacía, los miembros restantes se inicializan con valor. Si un miembro de un tipo de referencia es uno de estos miembros restantes, el programa está mal formado.

Otra opción es tomar la idea centinela de max66 y agregar un poco de azúcar sintáctica para facilitar la lectura.

struct init_list_guard
{
    struct ender {

    } static const end;
    init_list_guard() = delete;

    init_list_guard(ender e){ }
};

struct A
{
    char a;
    char b;
    char c;

    init_list_guard guard;
};

int main() {
   // A a1{ 1, 2 }; // will not compile 
   // A a2{ 1, init_list_guard::end }; // will not compile 
   A a3{ 1,2,3,init_list_guard::end }; // compiles OK
Gonen I
fuente
Desafortunadamente, esto hace que no se pueda Amover y cambia la semántica de copia ( Aya no es un agregado de valores, por así decirlo) :(
Johannes Schaub - litb
@ JohannesSchaub-litb OK. ¿Qué tal esta idea en mi respuesta editada?
Gonen I
@ JohannesSchaub-litb: igualmente importante, la primera versión agrega un nivel de indirección al hacer que los miembros apunten. Aún más importante, tienen que ser una referencia a algo, y los 1,2,3objetos son efectivamente locales en el almacenamiento automático que quedan fuera del alcance cuando finaliza la función. Y hace que el tamaño de (A) 24 en lugar de 3 en un sistema con punteros de 64 bits (como x86-64).
Peter Cordes
Una referencia ficticia aumenta el tamaño de 3 a 16 bytes (relleno para la alineación del miembro del puntero (referencia) + el puntero en sí). Siempre y cuando nunca use la referencia, probablemente esté bien si apunta a un objeto que se ha salido de alcance. Ciertamente me preocuparía que no se optimice, y copiarlo no lo hará. (Una clase vacía tiene una mejor oportunidad de optimizar aparte de su tamaño, por lo que la tercera opción aquí es la menos mala, pero aún así cuesta espacio en cada objeto, al menos en algunos ABI. Todavía me preocuparía por el daño del relleno optimización en algunos casos.)
Peter Cordes