É possível evitar a omissão de membros agregados de inicialização?

43

Eu tenho uma estrutura com muitos membros do mesmo tipo, como este

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

O problema é que, se eu esquecer de inicializar um dos membros struct (por exemplo wasactive), assim:

VariablePointers{activePtr, filename}

O compilador não irá reclamar disso, mas terei um objeto parcialmente inicializado. Como posso evitar esse tipo de erro? Eu poderia adicionar um construtor, mas duplicaria a lista de variáveis ​​duas vezes, então eu tenho que digitar tudo isso três vezes!

Adicione também respostas C ++ 11 , se houver uma solução para C ++ 11 (atualmente estou restrito a essa versão). Padrões linguísticos mais recentes também são bem-vindos!

Johannes Schaub - litb
fonte
6
Digitar um construtor não parece tão terrível. A menos que você tenha muitos membros, nesse caso, talvez a refatoração esteja em ordem.
Vou
11
@Someprogrammerdude Eu acho que ele quer dizer que o erro é que você pode acidentalmente omitir um valor de inicialização
Gonen I
2
@theWiseBro, se você souber como a matriz / vetor ajuda a publicar uma resposta. Não é tão óbvio, eu não o vejo
idclev 463035818
2
@Someprogrammerdude Mas isso é mesmo um aviso? Não é possível vê-lo com o VS2019.
acraig5075 10/02
8
Há um -Wmissing-field-initializerssinalizador de compilação.
Ron

Respostas:

42

Aqui está um truque que dispara um erro de vinculador se um inicializador necessário estiver ausente:

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;
}

Resultado:

/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

Ressalvas:

  • Antes do C ++ 14, isso evita que Fooseja agregado.
  • Tecnicamente, isso depende de comportamento indefinido (violação de ODR), mas deve funcionar em qualquer plataforma sã.
Quentin
fonte
Você pode excluir o operador de conversão e, em seguida, é um erro do compilador.
jrok 10/02
@jrok sim, mas é um assim que Fooé declarado, mesmo que você nunca chame o operador.
Quentin
2
@jrok Mas, em seguida, ele não compila, mesmo que a inicialização seja fornecida. godbolt.org/z/yHZNq_ Adendo: Para o MSVC, ele funciona como você descreveu: godbolt.org/z/uQSvDa Isso é um bug?
n314159 10/02
Claro, bobo eu.
jrok 10/02
6
Infelizmente, esse truque não funciona com o C ++ 11, pois ele se tornará não agregado :( Eu removi a tag C ++ 11, portanto sua resposta também é viável (por favor, não a exclua), mas uma solução C ++ 11 ainda é a preferida, se possível
Johannes Schaub - litb
22

Para clang e gcc, você pode compilar com -Werror=missing-field-initializersisso transforma o aviso em inicializadores de campo ausentes em um erro. godbolt

Edit: Para MSVC, parece não haver nenhum aviso emitido, mesmo no nível /Wall, por isso não acho possível avisar sobre a falta de inicializadores com este compilador. godbolt

n314159
fonte
7

Não é uma solução elegante e útil, suponho ... mas deve funcionar também com C ++ 11 e fornecer um erro em tempo de compilação (não em tempo de link).

A idéia é adicionar em sua estrutura um membro adicional, na última posição, de um tipo sem inicialização padrão (e que não pode ser inicializado com um valor do tipo VariablePtr(ou qualquer que seja o tipo dos valores anteriores)

Por exemplo

struct bar
 {
   bar () = delete;

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

   bar (int) 
    { }
 };

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

   bar sentinel;
 };

Dessa forma, você é forçado a adicionar todos os elementos em sua lista de inicialização agregada, incluindo o valor para inicializar explicitamente o último valor (um número inteiro para sentinel, no exemplo) ou você recebe um erro "chamada para o construtor excluído da 'barra'".

assim

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

compilar e

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

não.

Infelizmente também

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

não compila.

- EDITAR -

Conforme apontado pelo MSalters (obrigado), há um defeito (outro defeito) no meu exemplo original: um barvalor pode ser inicializado com um charvalor (que é convertível em int), portanto, funciona a seguinte inicialização

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

e isso pode ser altamente confuso.

Para evitar esse problema, adicionei o seguinte construtor de modelo excluído

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

portanto, a f4declaração anterior gera um erro de compilação porque o dvalor é interceptado pelo construtor de modelos excluído

max66
fonte
Obrigado, isso é legal! Não é perfeito, como você mencionou, e também foo f;falha na compilação, mas talvez isso seja mais um recurso do que uma falha nesse truque. Aceitará se não houver uma proposta melhor que essa.
Johannes Schaub - litb
11
Eu faria o construtor de barras aceitar um membro da classe constante aninhado chamado algo como init_list_end para facilitar a leitura
Gonen I
@ GonenI - para facilitar a leitura, você pode aceitar enume nomear init_list_end(ou simplesmente list_end) um valor disso enum; mas a legibilidade acrescenta muita máquina de escrever; portanto, dado que o valor adicional é o ponto fraco desta resposta, não sei se é uma boa ideia.
max66 11/02
Talvez adicione algo como constexpr static int eol = 0;no cabeçalho de bar. test{a, b, c, eol}parece bastante legível para mim.
n314159 11/02
@ n314159 - bem ... torne-se bar::eol; é quase como passar um enumvalor; mas não acho importante: o núcleo da resposta é "adicione em sua estrutura um membro adicional, na última posição, de um tipo sem inicialização padrão"; a barparte é apenas um exemplo trivial para mostrar que a solução funciona; o exato "tipo sem inicialização padrão" deve depender das circunstâncias (IMHO).
max66 11/02
4

Para o CppCoreCheck, existe uma regra para verificar exatamente isso, se todos os membros foram inicializados e isso pode ser transformado de aviso em erro - isso geralmente é geral em todo o programa.

Atualizar:

A regra que você deseja verificar faz parte da segurança de tipo Type.6:

Tipo 6: sempre inicialize uma variável de membro: sempre inicialize, possivelmente usando construtores padrão ou inicializadores de membro padrão.

darune
fonte
2

A maneira mais simples é não fornecer ao tipo dos membros um construtor no-arg:

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 

Outra opção: se seus membros são const &, você precisa inicializar todos eles:

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

Se você pode viver com um const & member fictício, você pode combinar isso com a ideia de sentinela do @ max66.

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

Se o número de cláusulas do inicializador for menor que o número de membros ou a lista do inicializador estiver completamente vazia, os membros restantes serão inicializados por valor. Se um membro de um tipo de referência é um desses membros restantes, o programa está incorreto.

Outra opção é pegar a ideia sentinela do max66 e adicionar um pouco de açúcar sintático para facilitar a leitura

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
Vou eu
fonte
Infelizmente, isso torna Ainabalável e altera a cópia-semântica ( Anão é mais um agregado de valores, por assim dizer) :(
Johannes Schaub - litb
@ JohannesSchaub-litb OK. Que tal essa idéia na minha resposta editada?
Vou I
@ JohannesSchaub-litb: igualmente importante, a primeira versão adiciona um nível de indireção, fazendo os membros apontarem. Ainda mais importante, eles precisam ser uma referência a algo, e os 1,2,3objetos são efetivamente locais no armazenamento automático que ficam fora do escopo quando a função termina. E cria o tamanho de (A) 24 em vez de 3 em um sistema com ponteiros de 64 bits (como x86-64).
Peter Cordes
Uma referência fictícia aumenta o tamanho de 3 para 16 bytes (preenchimento para alinhamento do membro do ponteiro (referência) + o próprio ponteiro.) Desde que você nunca use a referência, provavelmente tudo bem se apontar para um objeto que saiu do ar escopo. Eu certamente me preocuparia em não otimizar, e copiá-lo certamente não. (Uma classe vazia tem uma chance melhor de otimizar além de seu tamanho, portanto, a terceira opção aqui é a menos ruim, mas ainda custa espaço em todos os objetos, pelo menos em algumas ABIs. otimização em alguns casos.)
Peter Cordes