Czy można zapobiec pominięciu elementów inicjujących agregację?

43

Mam strukturę z wieloma członkami tego samego typu, jak ten

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

Problem polega na tym, że jeśli zapomnę zainicjować jednego z elementów struktury (np. wasactive), To tak:

VariablePointers{activePtr, filename}

Kompilator nie będzie na to narzekał, ale będę miał częściowo zainicjowany obiekt. Jak mogę zapobiec tego rodzaju błędom? Mógłbym dodać konstruktor, ale skopiowałby listę zmiennych dwukrotnie, więc muszę trzykrotnie wpisać to wszystko!

Dodaj również odpowiedzi C ++ 11 , jeśli istnieje rozwiązanie dla C ++ 11 (obecnie jestem ograniczony do tej wersji). Mile widziane są jednak nowsze standardy językowe!

Johannes Schaub - litb
źródło
6
Wpisanie konstruktora nie brzmi tak strasznie. Chyba że masz zbyt wielu członków, w takim przypadku może być konieczne refaktoryzacja.
Gonen I
1
@Someprogrammerdude Myślę, że chodzi mu o to, że można przypadkowo pominąć wartość inicjującą
Gonen I
2
@ theWiseBro, jeśli wiesz, w jaki sposób tablica / wektor pomaga opublikować odpowiedź. To nie jest takie oczywiste, nie widzę tego
idclev 463035818
2
@Someprogrammerdude Ale czy to w ogóle ostrzeżenie? Nie widać tego z VS2019.
acraig5075
8
Istnieje -Wmissing-field-initializersflaga kompilacji.
Ron

Odpowiedzi:

42

Oto sztuczka, która wyzwala błąd linkera, jeśli brakuje wymaganego inicjalizatora:

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

Stosowanie:

struct Foo {
    int bar = init_required;
};

int main() {
    Foo f;
}

Wynik:

/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

Ostrzeżenia:

  • Przed wersją C ++ 14 Foow ogóle nie jest to agregat.
  • To technicznie zależy od nieokreślonego zachowania (naruszenie ODR), ale powinno działać na każdej rozsądnej platformie.
Quentin
źródło
Możesz usunąć operator konwersji, a wtedy jest to błąd kompilatora.
żart
@jrok tak, ale jest taki, jak tylko Foozostanie zadeklarowany, nawet jeśli tak naprawdę nigdy nie zadzwonisz do operatora.
Quentin
2
@jrok Ale wtedy nie kompiluje się, nawet jeśli inicjalizacja jest zapewniona. godbolt.org/z/yHZNq_ Dodatek: W przypadku MSVC działa on tak, jak opisano: godbolt.org/z/uQSvDa Czy to błąd?
n314159
Oczywiście, głupie mnie.
żart
6
Niestety, ta sztuczka nie działa z C ++ 11, ponieważ stanie się wtedy nieskumulowana :( Usunąłem tag C ++ 11, więc twoja odpowiedź jest również wykonalna (proszę nie usuwaj), ale w miarę możliwości nadal preferowane jest rozwiązanie C ++ 11.
Johannes Schaub - litb
22

W przypadku clang i gcc można skompilować, -Werror=missing-field-initializersco powoduje, że ostrzeżenie o brakujących inicjatorach pól zmienia się w błąd. godbolt

Edycja: W przypadku MSVC wydaje się, że nie jest emitowane żadne ostrzeżenie nawet na poziomie /Wall, więc nie sądzę, że możliwe jest ostrzeżenie o brakujących inicjalizatorach za pomocą tego kompilatora. godbolt

n314159
źródło
7

Chyba nie jest to eleganckie i poręczne rozwiązanie ... ale powinno również działać z C ++ 11 i dawać błąd czasu kompilacji (nie łącza).

Chodzi o to, aby dodać do struktury dodatkowy element członkowski, na ostatniej pozycji, typu bez domyślnej inicjalizacji (i który nie może zainicjować wartością typu VariablePtr(lub jakikolwiek typ poprzednich wartości)

Przez przykład

struct bar
 {
   bar () = delete;

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

   bar (int) 
    { }
 };

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

   bar sentinel;
 };

W ten sposób jesteś zmuszony dodać wszystkie elementy do swojej zagregowanej listy inicjalizacyjnej, w tym wartość do jawnego zainicjowania ostatniej wartości (liczba całkowita sentinel, w przykładzie) lub otrzymujesz błąd „wywołanie usuniętego konstruktora błędu”.

Więc

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

skompiluj i

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

nie.

Niestety także

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

nie kompiluje się.

-- EDYTOWAĆ --

Jak wskazali MSalters (dzięki), w moim oryginalnym przykładzie występuje wada (kolejna wada): barwartość może być zainicjowana charwartością (którą można przekształcić int), więc działa następująca inicjalizacja

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

i może to być bardzo mylące.

Aby uniknąć tego problemu, dodałem następujący konstruktor usuniętych szablonów

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

więc poprzednia f4deklaracja podaje błąd kompilacji, ponieważ dwartość jest przechwytywana przez konstruktor szablonu, który jest usuwany

max66
źródło
Dzięki, to miło! To nie jest idealne, jak wspomniałeś, a także powoduje, że foo f;nie można go skompilować, ale może to bardziej cecha niż wada tej sztuczki. Akceptuje, jeśli nie ma lepszej propozycji niż ta.
Johannes Schaub - litb
1
Zmusiłbym konstruktora barów do zaakceptowania stałego elementu klasy zagnieżdżonej o nazwie coś takiego jak init_list_end dla czytelności
Gonen I
@ GonenI - dla czytelności możesz zaakceptować enumi nazwać init_list_end(po prostu list_end) jego wartość enum; ale czytelność dodaje dużo pisania na maszynie, więc biorąc pod uwagę, że dodatkowa wartość jest słabym punktem tej odpowiedzi, nie wiem, czy to dobry pomysł.
max66
Może dodaj coś constexpr static int eol = 0;w nagłówku bar. test{a, b, c, eol}wydaje mi się dość czytelny.
n314159
@ n314159 - cóż ... zostań bar::eol; jest prawie tak samo jak enumwartość; ale nie sądzę, żeby to było ważne: rdzeń odpowiedzi brzmi: „dodaj do swojej struktury dodatkowego członka, na ostatniej pozycji, typu bez domyślnej inicjalizacji”; barczęść jest po prostu banalny przykład, aby pokazać, że rozwiązanie działa; dokładny „typ bez domyślnej inicjalizacji” powinien zależeć od okoliczności (IMHO).
max66
4

W przypadku CppCoreCheck istnieje reguła sprawdzająca dokładnie, czy wszystkie elementy zostały zainicjowane i które można przekształcić z ostrzeżenia w błąd - zwykle dotyczy to całego programu.

Aktualizacja:

Reguła, którą chcesz sprawdzić, jest częścią bezpieczeństwa typów Type.6:

Typ 6: Zawsze inicjuj zmienną składową: zawsze inicjuj, prawdopodobnie używając domyślnych konstruktorów lub domyślnych inicjatorów składowych.

darune
źródło
2

Najprostszym sposobem jest nie nadanie typowi elementów konstruktora bez argumentu:

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 

Inna opcja: jeśli Twoi członkowie są const &, musisz zainicjować ich wszystkich:

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

Jeśli możesz żyć z jednym manekinem const i członkiem, możesz połączyć to z pomysłem @ max66 na wartownika.

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

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

Jeśli liczba klauzul inicjalizujących jest mniejsza niż liczba elementów lub lista inicjalizująca jest całkowicie pusta, pozostałe elementy są inicjowane pod względem wartości. Jeśli członkiem typu odniesienia jest jeden z tych pozostałych członków, program jest źle sformułowany.

Inną opcją jest wzięcie pomysłu wartownika max66 i dodanie cukru syntaktycznego dla czytelności

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.
źródło
Niestety, powoduje to, że jest Anieporuszalny i zmienia semantykę kopiowania ( Anie jest już zbiorem wartości, że tak powiem) :(
Johannes Schaub - litb
@ JohannesSchaub-litb OK. Co powiesz na ten pomysł w mojej zredagowanej odpowiedzi?
Gonen I
@ JohannesSchaub-litb: równie ważne jest to, że pierwsza wersja dodaje poziom pośredni, czyniąc wskaźniki członkami. Co ważniejsze, muszą być odniesieniem do czegoś, a 1,2,3obiekty są faktycznie lokalnymi obiektami w automatycznym magazynie, które wykraczają poza zakres po zakończeniu funkcji. I sprawia, że ​​sizeof (A) 24 zamiast 3 w systemie z 64-bitowymi wskaźnikami (jak x86-64).
Peter Cordes
Fikcyjne odniesienie zwiększa rozmiar z 3 do 16 bajtów (wypełnienie w celu wyrównania elementu wskaźnika (odniesienia) + sam wskaźnik). Tak długo, jak nigdy nie użyjesz odniesienia, prawdopodobnie jest w porządku, jeśli wskazuje na obiekt, który zniknął z zakres. Z pewnością martwiłbym się tym, że nie zoptymalizuje się, a kopiowanie go na pewno nie. (Pusta klasa ma większą szansę na optymalizację poza swoją wielkością, więc trzecia opcja tutaj jest najmniej zła, ale nadal kosztuje miejsce w każdym obiekcie przynajmniej w niektórych ABI. Wciąż martwiłbym się również o zranienie paddingiem optymalizacja w niektórych przypadkach.)
Peter Cordes