È possibile impedire l'omissione di membri di inizializzazione aggregati?


43

Ho una struttura con molti membri dello stesso tipo, come questo

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

Il problema è che se dimentico di inizializzare uno dei membri struct (ad es. wasactive), In questo modo:

VariablePointers{activePtr, filename}

Il compilatore non si lamenterà, ma avrò un oggetto parzialmente inizializzato. Come posso prevenire questo tipo di errore? Potrei aggiungere un costruttore, ma duplicerebbe due volte l'elenco di variabili, quindi devo digitare tutto questo tre volte!

Aggiungi anche le risposte C ++ 11 , se esiste una soluzione per C ++ 11 (attualmente sono limitato a quella versione). Anche gli standard linguistici più recenti sono benvenuti!


6
Digitare un costruttore non sembra così terribile. A meno che tu non abbia troppi membri, nel qual caso, forse il refactoring è in ordine.
Sono

1
@Someprogrammerdude Penso che significhi che l'errore è che puoi accidentalmente omettere un valore di inizializzazione
Gonen I

2
@theWiseBro se sai come array / vector ti aiuta a pubblicare una risposta. Non è così ovvio, non lo vedo
idclev 463035818,

2
@Someprogrammerdude Ma è anche un avvertimento? Non riesco a vederlo con VS2019.
acraig5075,

8
C'è un -Wmissing-field-initializersflag di compilazione.
Ron,

Risposte:


42

Ecco un trucco che attiva un errore del linker se manca un inizializzatore richiesto:

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

Risultato:

/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

Avvertenze:

  • Prima di C ++ 14, ciò impedisce Foodi essere un aggregato.
  • Questo si basa tecnicamente su comportamenti indefiniti (violazione ODR), ma dovrebbe funzionare su qualsiasi piattaforma sana.

È possibile eliminare l'operatore di conversione e quindi si tratta di un errore del compilatore.
jrok

@jrok sì, ma è uno non appena Fooviene dichiarato, anche se in realtà non si chiama l'operatore.
Quentin,

2
@jrok Ma poi non si compila anche se viene fornita l'inizializzazione. godbolt.org/z/yHZNq_ Addendum: Per MSVC funziona come descritto: godbolt.org/z/uQSvDa È un bug?
n314159

Certo, sciocco me.
jrok

6
Sfortunatamente, questo trucco non funziona con C ++ 11, poiché diventerà quindi non aggregato :( Ho rimosso il tag C ++ 11, quindi la tua risposta è valida anche (per favore non eliminarla), ma una soluzione C ++ 11 è ancora preferita, se possibile.
Johannes Schaub -

22

Per clang e gcc puoi compilare con -Werror=missing-field-initializersquello che trasforma l'avvertimento sugli inizializzatori di campo mancanti in un errore. Godbolt

Modifica: per MSVC, sembra che non ci siano avvisi emessi anche a livello /Wall, quindi non penso che sia possibile avvisare degli inizializzatori mancanti con questo compilatore. Godbolt


7

Non è una soluzione elegante e pratica, suppongo ... ma dovrebbe funzionare anche con C ++ 11 e dare un errore di compilazione (non link-time).

L'idea è quella di aggiungere nella struttura un membro aggiuntivo, nell'ultima posizione, di un tipo senza inizializzazione predefinita (e che non può essere inizializzato con un valore di tipo VariablePtr(o qualunque sia il tipo di valori precedenti)

Per esempio

struct bar
 {
   bar () = delete;

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

   bar (int) 
    { }
 };

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

   bar sentinel;
 };

In questo modo sei costretto ad aggiungere tutti gli elementi nel tuo elenco di inizializzazione aggregato, incluso il valore per inizializzare esplicitamente l'ultimo valore (un numero intero sentinel, nell'esempio) o ricevi una "chiamata al costruttore cancellato dell'errore" bar ".

Così

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

compilare e

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

non lo fa.

Sfortunatamente anche

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

non compilare.

-- MODIFICARE --

Come sottolineato da MSalters (grazie) c'è un difetto (un altro difetto) nel mio esempio originale: un barvalore potrebbe essere inizializzato con un charvalore (che è convertibile in int), quindi funziona la seguente inizializzazione

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

e questo può essere molto confuso.

Per evitare questo problema, ho aggiunto il seguente costruttore di modelli eliminati

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

quindi la f4dichiarazione precedente genera un errore di compilazione perché il dvalore viene intercettato dal costruttore del modello che viene eliminato


Grazie, è carino! Non è perfetto come hai già detto, e inoltre non foo f;riesce a compilare, ma forse è più una caratteristica che un difetto con questo trucco. Accetterà se non c'è proposta migliore di questa.
Johannes Schaub -

1
Vorrei che il costruttore di barre accettasse un membro della classe annidato chiamato qualcosa come init_list_end per leggibilità
Gonen I

@GonenI - per leggibilità puoi accettare un enum, e nominare init_list_end(o semplicemente list_end) un valore di quello enum; ma la leggibilità aggiunge molta macchina da scrivere, quindi, dato che il valore aggiuntivo è il punto debole di questa risposta, non so se sia una buona idea.
max66,

Forse aggiungere qualcosa di simile constexpr static int eol = 0;nell'intestazione di bar. test{a, b, c, eol}mi sembra abbastanza leggibile.
n314159

@ n314159 - beh ... diventa bar::eol; è quasi come passare un enumvalore; ma non credo sia importante: il nocciolo della risposta è "aggiungi nella tua struttura un membro aggiuntivo, nell'ultima posizione, di un tipo senza inizializzazione predefinita"; la barparte è solo un esempio banale per dimostrare che la soluzione funziona; l'esatto "tipo senza inizializzazione predefinita" dovrebbe dipendere dalle circostanze (IMHO).
max66,

4

Per CppCoreCheck esiste una regola per verificare esattamente che, se tutti i membri sono stati inizializzati e possono essere trasformati da avvertimento in un errore, che di solito è a livello di programma ovviamente.

Aggiornare:

La regola che si desidera verificare fa parte della sicurezza dei caratteri Type.6:

Tipo.6: inizializza sempre una variabile membro: inizializza sempre, possibilmente utilizzando costruttori predefiniti o inizializzatori membri predefiniti.


2

Il modo più semplice non è dare al tipo di membri un costruttore 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 

Un'altra opzione: se i tuoi membri sono const e, devi inizializzarli tutti:

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 riesci a vivere con una const e un membro fittizi, puoi combinarlo con l'idea di @ max66 di una sentinella.

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

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

Se il numero di clausole di inizializzazione è inferiore al numero di membri o l'elenco di inizializzatori è completamente vuoto, i membri rimanenti vengono inizializzati dal valore. Se un membro di un tipo di riferimento è uno di questi membri rimanenti, il programma è mal formato.

Un'altra opzione è prendere l'idea sentinella di max66 e aggiungere un po 'di zucchero sintattico per la leggibilità

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

Sfortunatamente, questo rende Ainamovibile e cambia la semantica della copia ( Anon è più un aggregato di valori, per così dire) :(
Johannes Schaub - litb

@ JohannesSchaub-litb OK. Che ne dici di questa idea nella mia risposta modificata?
Sono

@ JohannesSchaub-litb: altrettanto importante, la prima versione aggiunge un livello di riferimento indiretto rendendo i puntatori dei membri. Ancora più importante, devono essere un riferimento a qualcosa e gli 1,2,3oggetti sono effettivamente locali nell'archiviazione automatica che escono dall'ambito quando termina la funzione. E rende la dimensione di (A) 24 invece di 3 su un sistema con puntatori a 64 bit (come x86-64).
Peter Cordes,

Un riferimento fittizio aumenta la dimensione da 3 a 16 byte (riempimento per l'allineamento del membro puntatore (riferimento) + il puntatore stesso.) Fintanto che non si usa mai il riferimento, probabilmente va bene se punta a un oggetto che è uscito da scopo. Sicuramente mi preoccuperei che non si ottimizzerà, e copiarlo sicuramente non lo farà. (Una classe vuota ha una migliore possibilità di ottimizzazione diversa dalla sua dimensione, quindi la terza opzione qui è la meno cattiva, ma costa comunque spazio in ogni oggetto almeno in alcune ABI. Mi preoccuperei ancora del ferimento dell'imbottitura ottimizzazione in alcuni casi.)
Peter Cordes,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.