Bequeme C ++ - Strukturinitialisierung

140

Ich versuche einen bequemen Weg zu finden, um 'pod' C ++ - Strukturen zu initialisieren. Betrachten Sie nun die folgende Struktur:

struct FooBar {
  int foo;
  float bar;
};
// just to make all examples work in C and C++:
typedef struct FooBar FooBar;

Wenn ich dies bequem in C (!) Initialisieren möchte, könnte ich einfach schreiben:

/* A */ FooBar fb = { .foo = 12, .bar = 3.4 }; // illegal C++, legal C

Beachten Sie, dass ich die folgende Notation explizit vermeiden möchte, da es mir so vorkommt, als würde ich mir den Hals brechen, wenn ich in Zukunft etwas an der Struktur ändere :

/* B */ FooBar fb = { 12, 3.4 }; // legal C++, legal C, bad style?

Um in C ++ dasselbe (oder zumindest ein ähnliches) wie im /* A */Beispiel zu erreichen, müsste ich einen idiotischen Konstruktor implementieren:

FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) {}
// ->
/* C */ FooBar fb(12, 3.4);

Was gut zum Kochen von Wasser ist, aber nicht für faule Leute geeignet (Faulheit ist eine gute Sache, oder?). Außerdem ist es so schlecht wie das /* B */Beispiel, da nicht explizit angegeben wird, welcher Wert an welches Mitglied geht.

Meine Frage ist also im Grunde, wie ich /* A */in C ++ etwas Ähnliches oder Besseres erreichen kann . Alternativ wäre ich mit einer Erklärung einverstanden, warum ich dies nicht tun möchte (dh warum mein mentales Paradigma schlecht ist).

BEARBEITEN

Mit bequem meine ich auch wartbar und nicht redundant .

Bitmaske
quelle
2
Ich denke, das B-Beispiel ist so nah wie möglich.
Marlon
2
Ich sehe nicht, wie Beispiel B "schlechter Stil" ist. Für mich ist dies sinnvoll, da Sie jedes Mitglied der Reihe nach mit seinen jeweiligen Werten initialisieren.
Mike Bailey
25
Mike, es ist ein schlechter Stil, weil nicht klar ist, welcher Wert an welches Mitglied geht. Sie müssen sich die Definition der Struktur ansehen und dann die Mitglieder zählen, um herauszufinden, was jeder Wert bedeutet.
jnnnnn
9
Sollte sich die Definition von FooBar in Zukunft ändern, könnte die Initialisierung unterbrochen werden.
Edward Falk
Wenn die Initialisierung lang und komplex wird, vergessen Sie nicht das Builder-Muster
Schlitten

Antworten:

20

Bestimmte Initialisierungen werden in c ++ 2a unterstützt, aber Sie müssen nicht warten, da sie offiziell von GCC, Clang und MSVC unterstützt werden.

#include <iostream>
#include <filesystem>

struct hello_world {
    const char* hello;
    const char* world;
};

int main () 
{
    hello_world hw = {
        .hello = "hello, ",
        .world = "world!"
    };

    std::cout << hw.hello << hw.world << std::endl;
    return 0;
}

GCC Demo MSVC Demo

ivaigult
quelle
Vorsichtsmaßnahme: Beachten Sie, dass alte Initialisierungen, wenn Sie später Parameter am Ende der Struktur hinzufügen, weiterhin stillschweigend kompiliert werden, ohne initialisiert worden zu sein.
Catskul
1
@Catskul Nein. Es wird mit einer leeren Initialisierungsliste initialisiert, was zu einer Initialisierung mit Null führt.
ivaigult
Du hast recht. Danke dir. Ich sollte klarstellen, dass die verbleibenden Parameter stillschweigend standardmäßig initialisiert werden. Der Punkt, den ich ansprechen wollte, war, dass jeder, der hofft, dies könnte dazu beitragen, eine vollständige explizite Initialisierung der POD-Typen durchzusetzen, enttäuscht sein wird.
Catskul
43

Schon seit style A ist in C ++ nicht erlaubt und du willst style Bdann nicht wie wäre es mit style BX:

FooBar fb = { /*.foo=*/ 12, /*.bar=*/ 3.4 };  // :)

Zumindest einigermaßen helfen.

iammilind
quelle
8
+1: Es stellt nicht wirklich die korrekte Initialisierung sicher (vom Compiler-POV), hilft aber dem Leser sicher ... obwohl die Kommentare synchron gehalten werden sollten.
Matthieu M.
18
Ein Kommentar verhindert nicht, dass die Initialisierung der Struktur unterbrochen wird, wenn ich zwischen foound barin Zukunft ein neues Feld einfüge. C würde weiterhin die gewünschten Felder initialisieren, C ++ jedoch nicht. Und das ist der Punkt der Frage - wie man in C ++ das gleiche Ergebnis erzielt. Ich meine, Python macht das mit benannten Argumenten, C - mit "benannten" Feldern, und C ++ sollte hoffentlich auch etwas haben.
dmitry_romanov
2
Kommentare synchron? Gib mir eine Pause. Sicherheit geht durch das Fenster. Ordnen Sie die Parameter und den Ausleger neu an. Viel besser mit explicit FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) . Beachten Sie das explizite Schlüsselwort. Selbst ein Verstoß gegen den Standard ist in Bezug auf die Sicherheit besser. In Clang: -Wno-c99-Erweiterungen
Daniel O
@ DanielW, es geht nicht darum, was besser ist oder was nicht. Diese Antwort entspricht, dass das OP nicht Stil A (nicht c ++), B oder C will, der alle gültigen Fälle abdeckt.
Iammilind
@iammilind Ich denke, ein Hinweis darauf, warum das mentale Paradigma von OP schlecht ist, könnte die Antwort verbessern. Ich halte das für gefährlich, so wie es jetzt ist.
Daerst
10

Sie könnten ein Lambda verwenden:

const FooBar fb = [&] {
    FooBar fb;
    fb.foo = 12;
    fb.bar = 3.4;
    return fb;
}();

Weitere Informationen zu dieser Redewendung finden Sie im Blog von Herb Sutter .

Wimper
quelle
1
Ein solcher Ansatz initialisiert Felder zweimal. Einmal im Konstruktor. Zweitens ist fb.XXX = YYY.
Dmytro Ovdiienko
9

Extrahieren Sie die Contants in Funktionen, die sie beschreiben (grundlegendes Refactoring):

FooBar fb = { foo(), bar() };

Ich weiß, dass der Stil dem sehr nahe kommt, den Sie nicht verwenden wollten, aber er ermöglicht das einfachere Ersetzen der konstanten Werte und erklärt sie auch (sodass keine Kommentare bearbeitet werden müssen), falls sie sich jemals ändern.

Eine andere Möglichkeit (da Sie faul sind) besteht darin, den Konstruktor inline zu machen, damit Sie nicht so viel eingeben müssen (Entfernen von "Foobar ::" und Zeitaufwand für das Umschalten zwischen h- und cpp-Datei):

struct FooBar {
  FooBar(int f, float b) : foo(f), bar(b) {}
  int foo;
  float bar;
};
ralphtheninja
quelle
1
Ich empfehle jedem anderen, der diese Frage liest, den Stil im unteren Code-Snippet für diese Antwort zu wählen, wenn Sie nur in der Lage sein möchten, Strukturen schnell mit einer Reihe von Werten zu initialisieren.
KayleeFrye_onDeck
8

Ihre Frage ist etwas schwierig, weil sogar die Funktion:

static FooBar MakeFooBar(int foo, float bar);

kann genannt werden als:

FooBar fb = MakeFooBar(3.4, 5);

aufgrund der Regeln für Werbung und Konvertierung für integrierte numerische Typen. (C wurde nie wirklich stark getippt)

In C ++ ist das, was Sie wollen, mit Hilfe von Vorlagen und statischen Zusicherungen erreichbar:

template <typename Integer, typename Real>
FooBar MakeFooBar(Integer foo, Real bar) {
  static_assert(std::is_same<Integer, int>::value, "foo should be of type int");
  static_assert(std::is_same<Real, float>::value, "bar should be of type float");
  return { foo, bar };
}

In C können Sie die Parameter benennen, aber Sie werden nie weiter kommen.

Wenn Sie jedoch nur benannte Parameter verwenden möchten, schreiben Sie viel umständlichen Code:

struct FooBarMaker {
  FooBarMaker(int f): _f(f) {}
  FooBar Bar(float b) const { return FooBar(_f, b); }
  int _f;
};

static FooBarMaker Foo(int f) { return FooBarMaker(f); }

// Usage
FooBar fb = Foo(5).Bar(3.4);

Und wenn Sie möchten, können Sie den Schutz der Typwerbung verbessern.

Matthieu M.
quelle
1
"In C ++ ist das, was Sie wollen, erreichbar": Hat OP nicht darum gebeten, die Verwechslung der Reihenfolge der Parameter zu verhindern? Wie würde die von Ihnen vorgeschlagene Vorlage dies erreichen? Nehmen wir zur Vereinfachung an, wir haben zwei Parameter, beide int.
Max
@max: Dies wird nur verhindert, wenn sich die Typen unterscheiden (auch wenn sie untereinander konvertierbar sind). Dies ist die OP-Situation. Wenn es die Typen nicht unterscheiden kann, funktioniert es natürlich nicht, aber das ist eine ganz andere Frage.
Matthieu M.
Ich habe es verstanden. Ja, dies sind zwei verschiedene Probleme, und ich denke, das zweite hat derzeit keine gute Lösung in C ++ (aber es scheint, dass C ++ 20 die Unterstützung für die Parameternamen im C99-Stil in der aggregierten Initialisierung hinzufügt).
Max
6

Die C ++ - Frontends vieler Compiler (einschließlich GCC und Clang) verstehen die C-Initialisierersyntax. Wenn Sie können, verwenden Sie einfach diese Methode.

Matthias Urlichs
quelle
16
Was nicht dem C ++ Standard entspricht!
Bitmaske
5
Ich weiß, dass es nicht Standard ist. Aber wenn Sie es verwenden können, ist es immer noch der sinnvollste Weg, eine Struktur zu initialisieren.
Matthias Urlichs
2
Sie können x- und y-Typen schützen, indem Sie den falschen Konstruktor privat machen:private: FooBar(float x, int y) {};
dmitry_romanov
4
clang (llvm-basierter c ++ - Compiler) unterstützt diese Syntax ebenfalls. Schade, dass es nicht zum Standard gehört.
Nimrodm
Wir alle wissen, dass C-Initialisierer nicht Teil des C ++ - Standards sind. Aber viele Compiler verstehen es und die Frage hat nicht gesagt, auf welchen Compiler abgezielt wird, wenn überhaupt. Bitte stimmen Sie diese Antwort nicht ab.
Matthias Urlichs
4

Ein weiterer Weg in C ++ ist

struct Point
{
private:

 int x;
 int y;

public:
    Point& setX(int xIn) { x = Xin; return *this;}
    Point& setY(int yIn) { y = Yin; return *this;}

}

Point pt;
pt.setX(20).setY(20);
parapura rajkumar
quelle
2
Umständlich für die funktionale Programmierung (dh das Erstellen des Objekts in der Argumentliste eines Funktionsaufrufs), aber ansonsten wirklich eine nette Idee!
Bitmaske
27
Der Optimierer reduziert es wahrscheinlich, aber meine Augen nicht.
Matthieu M.
6
Zwei Wörter: argh ... argh! Wie ist das besser als die Verwendung öffentlicher Daten mit 'Point pt; pt.x = pt.y = 20; `? Oder wenn Sie eine Kapselung wünschen, wie ist dies besser als ein Konstruktor?
OldPeculier
3
Es ist besser als ein Konstruktor, weil Sie sich die Konstruktordeklaration für die Parameterreihenfolge ansehen müssen ... ist es x, y oder y, x, aber die Art und Weise, wie ich es gezeigt habe, ist am
Aufrufort offensichtlich
2
Dies funktioniert nicht, wenn Sie eine const struct möchten. oder wenn Sie dem Compiler mitteilen möchten, dass er nicht initialisierte Strukturen nicht zulässt. Wenn Sie es wirklich so machen wollen, markieren Sie zumindest die Setter mit inline!
Matthias Urlichs
3

Option D:

FooBar FooBarMake(int foo, float bar)

Legal C, legal C ++. Leicht für PODs optimierbar. Natürlich gibt es keine benannten Argumente, aber das ist wie bei allen C ++. Wenn Sie benannte Argumente möchten, sollte Ziel C die bessere Wahl sein.

Option E:

FooBar fb;
memset(&fb, 0, sizeof(FooBar));
fb.foo = 4;
fb.bar = 15.5f;

Legal C, legal C ++. Benannte Argumente.

John
quelle
12
Anstelle von Memset, das Sie FooBar fb = {};in C ++ verwenden können, werden standardmäßig alle Strukturelemente initialisiert.
Öö Tiib
@ ÖöTiib: Leider ist das aber illegales C.
CB Bailey
3

Ich weiß, dass diese Frage alt ist, aber es gibt eine Möglichkeit, dies zu lösen, bis C ++ 20 diese Funktion endlich von C nach C ++ bringt. Um dies zu lösen, können Sie Präprozessor-Makros mit static_asserts verwenden, um zu überprüfen, ob Ihre Initialisierung gültig ist. (Ich weiß, dass Makros im Allgemeinen schlecht sind, aber hier sehe ich keinen anderen Weg.) Siehe Beispielcode unten:

#define INVALID_STRUCT_ERROR "Instantiation of struct failed: Type, order or number of attributes is wrong."

#define CREATE_STRUCT_1(type, identifier, m_1, p_1) \
{ p_1 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_2(type, identifier, m_1, p_1, m_2, p_2) \
{ p_1, p_2 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_3(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3) \
{ p_1, p_2, p_3 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_4(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3, m_4, p_4) \
{ p_1, p_2, p_3, p_4 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_4) >= (offsetof(type, m_3) + sizeof(identifier.m_3)), INVALID_STRUCT_ERROR);\

// Create more macros for structs with more attributes...

Wenn Sie dann eine Struktur mit const-Attributen haben, können Sie dies tun:

struct MyStruct
{
    const int attr1;
    const float attr2;
    const double attr3;
};

const MyStruct test = CREATE_STRUCT_3(MyStruct, test, attr1, 1, attr2, 2.f, attr3, 3.);

Dies ist etwas unpraktisch, da Sie Makros für jede mögliche Anzahl von Attributen benötigen und den Typ und Namen Ihrer Instanz im Makroaufruf wiederholen müssen. Sie können das Makro auch nicht in einer return-Anweisung verwenden, da die Asserts nach der Initialisierung erfolgen.

Aber es löst Ihr Problem: Wenn Sie die Struktur ändern, schlägt der Aufruf beim Kompilieren fehl.

Wenn Sie C ++ 17 verwenden, können Sie diese Makros sogar strenger gestalten, indem Sie dieselben Typen erzwingen, z.

#define CREATE_STRUCT_3(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3) \
{ p_1, p_2, p_3 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_1) == typeid(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_2) == typeid(identifier.m_2), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_3) == typeid(identifier.m_3), INVALID_STRUCT_ERROR);\
Max Vollmer
quelle
Gibt es einen C ++ 20-Vorschlag, um die genannten Initialisierer zuzulassen?
Maël Nison
1
@ MaëlNison Ja: Designated Initialisierer (seit C ++ 20)
Max Vollmer
2

Der Weg /* B */ ist in C ++ in Ordnung, auch das C ++ 0x wird die Syntax erweitern, so dass es auch für C ++ - Container nützlich ist. Ich verstehe nicht, warum du es schlechten Stil nennst?

Wenn Sie Parameter mit Namen angeben möchten, können Sie die Boost-Parameterbibliothek verwenden , die jedoch jemanden verwirren kann, der damit nicht vertraut ist.

Das Neuordnen von Strukturelementen ähnelt dem Neuordnen von Funktionsparametern. Ein solches Refactoring kann Probleme verursachen, wenn Sie es nicht sehr sorgfältig ausführen.

Öö Tiib
quelle
7
Ich nenne es schlechten Stil, weil ich denke, dass es null wartbar ist. Was ist, wenn ich in einem Jahr ein weiteres Mitglied hinzufüge? Oder wenn ich die Reihenfolge / Typen der Mitglieder ändere? Jeder Code, der ihn initialisiert, kann (sehr wahrscheinlich) kaputt gehen.
Bitmaske
2
@bitmask Aber solange Sie keine Argumente benannt haben, müssten Sie auch Konstruktoraufrufe aktualisieren, und ich denke, nicht viele Leute denken, Konstruktoren seien ein nicht zu wartender schlechter Stil. Ich denke auch, dass die benannte Initialisierung nicht C ist, sondern C99, von dem C ++ definitiv keine Obermenge ist.
Christian Rau
2
Wenn Sie innerhalb eines Jahres bis zum Ende der Struktur ein weiteres Mitglied hinzufügen, wird es in bereits vorhandenem Code standardmäßig initialisiert. Wenn Sie sie neu anordnen, müssen Sie den gesamten vorhandenen Code bearbeiten, nichts zu tun.
Öö Tiib
1
@bitmask: Das erste Beispiel wäre dann auch "nicht wartbar". Was passiert, wenn Sie stattdessen eine Variable in der Struktur umbenennen? Sicher, Sie könnten alles ersetzen, aber das könnte versehentlich eine Variable umbenennen, die nicht umbenannt werden sollte.
Mike Bailey
@ChristianRau Seit wann ist C99 nicht C? Ist C nicht die Gruppe und C99 eine bestimmte Version / ISO-Spezifikation?
Altendky
1

Was ist mit dieser Syntax?

typedef struct
{
    int a;
    short b;
}
ABCD;

ABCD abc = { abc.a = 5, abc.b = 7 };

Gerade auf einem Microsoft Visual C ++ 2015 und auf g ++ 6.0.2 getestet. OK funktionieren.
Sie können ein bestimmtes Makro auch erstellen, wenn Sie das Duplizieren des Variablennamens vermeiden möchten.

cls
quelle
clang++3.5.0-10 mit -Weverything -std=c++1zscheint das zu bestätigen. Aber es sieht nicht richtig aus. Wissen Sie, wo der Standard bestätigt, dass dies gültiges C ++ ist?
Bitmaske
Ich weiß es nicht, aber ich habe das seit langer Zeit in verschiedenen Compilern verwendet und keine Probleme gesehen. Jetzt auf g ++ 4.4.7 getestet - funktioniert gut.
Cls
5
Ich denke nicht, dass diese Arbeit. Versuchen Sie es ABCD abc = { abc.b = 7, abc.a = 5 };.
Raymai97
@deselect funktioniert, weil das Feld mit dem Wert initialisiert wird, der vom Operator = zurückgegeben wird. Tatsächlich initialisieren Sie das Klassenmitglied also zweimal.
Dmytro Ovdiienko
1

Für mich ist die Verwendung dieses Makros der faulste Weg, eine Inline-Inizialisierung zuzulassen.

#define METHOD_MEMBER(TYPE, NAME, CLASS) \
CLASS &set_ ## NAME(const TYPE &_val) { NAME = _val; return *this; } \
TYPE NAME;

struct foo {
    METHOD_MEMBER(string, attr1, foo)
    METHOD_MEMBER(int, attr2, foo)
    METHOD_MEMBER(double, attr3, foo)
};

// inline usage
foo test = foo().set_attr1("hi").set_attr2(22).set_attr3(3.14);

Dieses Makro erstellt ein Attribut und eine Selbstreferenzmethode.

Emanuele Pavanello
quelle