Ist es möglich, das Auslassen aggregierter Initialisierungsmitglieder zu verhindern?

43

Ich habe eine Struktur mit vielen Mitgliedern des gleichen Typs, wie diese

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

Das Problem ist, dass, wenn ich vergesse, eines der Strukturelemente (z. B. wasactive) wie folgt zu initialisieren :

VariablePointers{activePtr, filename}

Der Compiler wird sich nicht darüber beschweren, aber ich werde ein Objekt haben, das teilweise initialisiert ist. Wie kann ich diese Art von Fehler verhindern? Ich könnte einen Konstruktor hinzufügen, aber er würde die Liste der Variablen zweimal duplizieren, also muss ich das alles dreimal eingeben!

Bitte fügen Sie auch C ++ 11- Antworten hinzu, wenn es eine Lösung für C ++ 11 gibt (derzeit bin ich auf diese Version beschränkt). Neuere Sprachstandards sind jedoch ebenfalls willkommen!

Johannes Schaub - litb
quelle
6
Die Eingabe eines Konstruktors klingt nicht so schrecklich. Wenn Sie nicht zu viele Mitglieder haben, ist in diesem Fall möglicherweise ein Refactoring angebracht.
Gonen I
1
@Someprogrammerdude Ich denke, er meint, der Fehler ist, dass Sie versehentlich einen Initialisierungswert weglassen können
Gonen I
2
@theWiseBro Wenn Sie wissen, wie Array / Vektor Ihnen hilft, sollten Sie eine Antwort posten. Es ist nicht so offensichtlich, ich sehe es nicht
idclev 463035818
2
@Someprogrammerdude Aber ist es überhaupt eine Warnung? Kann es mit VS2019 nicht sehen.
acraig5075
8
Es gibt ein -Wmissing-field-initializersKompilierungsflag.
Ron

Antworten:

42

Hier ist ein Trick, der einen Linkerfehler auslöst, wenn ein erforderlicher Initialisierer fehlt:

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

Verwendungszweck:

struct Foo {
    int bar = init_required;
};

int main() {
    Foo f;
}

Ergebnis:

/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

Vorsichtsmaßnahmen:

  • Vor C ++ 14 wird dadurch verhindert, dass es sich Fooüberhaupt um ein Aggregat handelt.
  • Dies beruht technisch auf undefiniertem Verhalten (ODR-Verletzung), sollte jedoch auf jeder vernünftigen Plattform funktionieren.
QUentin
quelle
Sie können den Konvertierungsoperator löschen und dann ist es ein Compilerfehler.
Jrok
@jrok ja, aber es ist eines, sobald Fooes deklariert ist, auch wenn Sie den Operator nie wirklich anrufen.
Quentin
2
@jrok Aber dann wird es nicht kompiliert, selbst wenn die Initialisierung bereitgestellt wird. godbolt.org/z/yHZNq_ Nachtrag: Für MSVC funktioniert es wie beschrieben: godbolt.org/z/uQSvDa Ist das ein Fehler?
n314159
Natürlich dumm mich.
Jrok
6
Leider funktioniert dieser Trick mit C ++ 11 nicht, da er dann nicht aggregiert wird :( Ich habe das C ++ 11-Tag entfernt, sodass Ihre Antwort auch brauchbar ist (bitte nicht löschen), aber Wenn möglich, wird weiterhin eine C ++ 11-Lösung bevorzugt.
Johannes Schaub - litb
22

Für clang und gcc können Sie kompilieren -Werror=missing-field-initializers, wodurch die Warnung bei fehlenden Feldinitialisierern in einen Fehler umgewandelt wird. Godbolt

Bearbeiten: Für MSVC scheint selbst auf Ebene keine Warnung ausgegeben zu werden /Wall, daher glaube ich nicht, dass es möglich ist, mit diesem Compiler vor fehlenden Initialisierern zu warnen. Godbolt

n314159
quelle
7

Keine elegante und handliche Lösung, nehme ich an ... sollte aber auch mit C ++ 11 funktionieren und einen Fehler bei der Kompilierung (nicht bei der Verbindungszeit) verursachen.

Die Idee ist, in Ihrer Struktur an der letzten Position ein zusätzliches Element eines Typs ohne Standardinitialisierung hinzuzufügen (und das nicht mit einem Wert vom Typ initialisiert werden kann VariablePtr (oder was auch immer der Typ der vorhergehenden Werte ist).

Zum Beispiel

struct bar
 {
   bar () = delete;

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

   bar (int) 
    { }
 };

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

   bar sentinel;
 };

Auf diese Weise müssen Sie alle Elemente zu Ihrer aggregierten Initialisierungsliste hinzufügen, den Wert zum expliziten Initialisieren des letzten Werts einschließen ( sentinelim Beispiel eine Ganzzahl für ), oder Sie erhalten den Fehler "Aufruf an gelöschten Konstruktor des Balkens".

Damit

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

kompilieren und

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

nicht.

Leider auch

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

kompiliert nicht.

- BEARBEITEN -

Wie von MSalters (danke) angegeben, gibt es in meinem ursprünglichen Beispiel einen Fehler (einen weiteren Fehler): Ein barWert könnte mit einem charWert initialisiert werden (der in konvertierbar ist int), daher funktioniert die folgende Initialisierung

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

und das kann sehr verwirrend sein.

Um dieses Problem zu vermeiden, habe ich den folgenden gelöschten Vorlagenkonstruktor hinzugefügt

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

Daher f4gibt die vorhergehende Deklaration einen Kompilierungsfehler aus, da der dWert vom gelöschten Vorlagenkonstruktor abgefangen wird

max66
quelle
Danke, das ist schön! Es ist nicht perfekt, wie Sie erwähnt haben, und führt auch dazu, dass es foo f;nicht kompiliert werden kann, aber vielleicht ist das eher eine Funktion als ein Fehler bei diesem Trick. Wird akzeptieren, wenn es keinen besseren Vorschlag als diesen gibt.
Johannes Schaub - 10.
1
Ich würde den Balkenkonstruktor veranlassen, ein mit Konstanten verschachteltes Klassenmitglied namens init_list_end zur besseren Lesbarkeit zu akzeptieren
Gonen I
@GonenI - zur besseren Lesbarkeit können Sie einen Wert akzeptieren enumund init_list_end(o einfach list_end) einen Wert davon benennen enum; Aber die Lesbarkeit fügt viel Schreibmaschine hinzu. Da der zusätzliche Wert der Schwachpunkt dieser Antwort ist, weiß ich nicht, ob es eine gute Idee ist.
max66
Vielleicht etwas wie constexpr static int eol = 0;in der Kopfzeile von hinzufügen bar. test{a, b, c, eol}scheint mir ziemlich lesbar.
n314159
@ n314159 - na ja ... werden bar::eol; es ist fast so, als würde man einen enumWert übergeben; aber ich denke nicht, dass es wichtig ist: Der Kern der Antwort ist "füge in deiner Struktur ein zusätzliches Mitglied an der letzten Position eines Typs ohne Standardinitialisierung hinzu"; Der barTeil ist nur ein triviales Beispiel, um zu zeigen, dass die Lösung funktioniert. Der genaue "Typ ohne Standardinitialisierung" sollte von den Umständen abhängen (IMHO).
max66
4

Für CppCoreCheck gibt es eine Regel, mit der genau überprüft werden kann, ob alle Mitglieder initialisiert wurden und die von einer Warnung in einen Fehler umgewandelt werden kann - das ist normalerweise normalerweise programmweit.

Aktualisieren:

Die Regel, die Sie überprüfen möchten, ist Teil der Typensicherheit Type.6:

Typ.6: Immer eine Mitgliedsvariable initialisieren: Immer initialisieren, möglicherweise mit Standardkonstruktoren oder Standardelementinitialisierern.

Darune
quelle
2

Der einfachste Weg ist, dem Typ der Mitglieder keinen Konstruktor ohne Argumente zu geben:

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 

Eine weitere Option: Wenn Ihre Mitglieder const & sind, müssen Sie alle initialisieren:

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

Wenn Sie mit einem Dummy const & member leben können, können Sie dies mit der Idee von @ max66 eines Sentinels kombinieren.

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

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

Wenn die Anzahl der Initialisierungsklauseln geringer ist als die Anzahl der Mitglieder oder die Initialisierungsliste vollständig leer ist, werden die verbleibenden Mitglieder wertinitialisiert. Wenn ein Mitglied eines Referenztyps eines dieser verbleibenden Mitglieder ist, ist das Programm fehlerhaft.

Eine andere Möglichkeit besteht darin, die Sentinel-Idee von max66 zu übernehmen und zur besseren Lesbarkeit etwas syntaktischen Zucker hinzuzufügen

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.
quelle
Leider macht dies Aunbeweglich und verändert die Kopiersemantik ( Aist sozusagen kein Aggregat von Werten mehr) :(
Johannes Schaub - litb
@ JohannesSchaub-litb OK. Wie wäre es mit dieser Idee in meiner bearbeiteten Antwort?
Gonen I
@ JohannesSchaub-litb: Ebenso wichtig ist, dass die erste Version eine Indirektionsebene hinzufügt, indem die Mitglieder auf Zeiger verweisen. Noch wichtiger ist, dass sie auf etwas verweisen müssen, und die 1,2,3Objekte sind effektiv lokale Objekte im automatischen Speicher, die nach Beendigung der Funktion den Gültigkeitsbereich verlassen. Auf einem System mit 64-Bit-Zeigern (wie x86-64) wird die Größe von (A) 24 anstelle von 3 festgelegt.
Peter Cordes
Eine Dummy-Referenz erhöht die Größe von 3 auf 16 Byte (Auffüllen für die Ausrichtung des Zeigerelements (Referenzelements) + des Zeigers selbst). Solange Sie die Referenz nie verwenden, ist es wahrscheinlich in Ordnung, wenn sie auf ein Objekt zeigt, das nicht mehr vorhanden ist Umfang. Ich würde mir sicherlich Sorgen machen, dass es nicht wegoptimiert wird, und das Kopieren wird es sicherlich nicht. (Eine leere Klasse hat eine bessere Chance, andere als ihre Größe zu optimieren. Daher ist die dritte Option hier die am wenigsten schlechte, aber sie kostet immer noch Platz in jedem Objekt, zumindest in einigen ABIs. Ich würde mir immer noch Sorgen machen, dass die Polsterung weh tut Optimierung in einigen Fällen.)
Peter Cordes