Comoda inizializzazione della struttura in C ++


141

Sto cercando di trovare un modo conveniente per inizializzare le strutture C ++ 'pod'. Ora, considera la seguente struttura:

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

Se voglio inizializzare comodamente questo in C (!), Potrei semplicemente scrivere:

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

Nota che voglio evitare esplicitamente la seguente notazione, perché mi sembra che mi sia fatto spezzare il collo se in futuro cambio qualcosa nella struttura:

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

Per ottenere lo stesso (o almeno simile) in C ++ come /* A */nell'esempio, dovrei implementare un costruttore idiota:

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

Che è buono per l'acqua bollente, ma non adatto per le persone pigre (la pigrizia è una buona cosa, giusto?). Inoltre, è praticamente negativo come /* B */nell'esempio, in quanto non indica esplicitamente quale valore va a quale membro.

Quindi, la mia domanda è sostanzialmente come posso ottenere qualcosa di simile /* A */o migliore in C ++? In alternativa, starei bene con una spiegazione del perché non dovrei voler fare questo (cioè perché il mio paradigma mentale è cattivo).

MODIFICARE

Per conveniente intendo anche mantenibile e non ridondante .


2
Penso che l'esempio B sia il più vicino possibile.
Marlon,

2
Non vedo come l'esempio B sia "cattivo stile". Per me ha senso, dato che stai inizializzando a turno ciascun membro con i rispettivi valori.
Mike Bailey,

26
Mike, è cattivo stile perché non è chiaro quale valore vada a quale membro. Devi andare a guardare la definizione della struttura e quindi contare i membri per trovare il significato di ciascun valore.
jnnnnn,

9
Inoltre, se la definizione di FooBar dovesse cambiare in futuro, l'inizializzazione potrebbe spezzarsi.
Edward Falk,

se l'inizializzazione diventa lunga e complessa, non dimenticare il modello del costruttore
slitta

Risposte:


20

Le inizializzazioni designate saranno supportate in c ++ 2a, ma non devi aspettare, perché sono ufficialmente supportate da GCC, Clang e MSVC.

#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


Avvertenza: tieni presente che se aggiungi parametri alla fine della struttura in un secondo momento, le vecchie inizializzazioni verranno comunque compilate silenziosamente senza essere state inizializzate.
Catskul,

1
@Catskul No. Verrà inizializzato con un elenco di inizializzatori vuoto, che comporterà l'inizializzazione con zero.
ivaigult,

Hai ragione. Grazie. Devo chiarire che i restanti parametri verranno automaticamente inizializzati in modo predefinito. Il punto che intendevo sottolineare era che chiunque spera che ciò possa aiutare a far rispettare l'inizializzazione esplicita completa dei tipi di POD rimarrà deluso.
Catskul,

43

Poiché style Anon è consentito in C ++ e non si desidera style Bquindi che ne dite di usare style BX:

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

Almeno aiuto in una certa misura.


8
+1: non garantisce realmente la corretta inizializzazione (dal compilatore POV) ma aiuta sicuramente il lettore ... anche se i commenti dovrebbero essere mantenuti sincronizzati.
Matthieu M.

18
Il commento non impedisce che l'inizializzazione della struttura venga interrotta se inserisco un nuovo campo tra fooe barin futuro. C continuerebbe a inizializzare i campi desiderati, ma C ++ no. E questo è il punto della domanda: come ottenere lo stesso risultato in C ++. Voglio dire, Python lo fa con argomenti con nome, C - con campi "con nome", e C ++ dovrebbe avere anche qualcosa, spero.
dmitry_romanov,

2
Commenti sincronizzati? Dammi una pausa. La sicurezza passa attraverso la finestra. Riordina i parametri e il boom. Molto meglio con explicit FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) . Nota la parola chiave esplicita . Perfino infrangere lo standard è meglio per quanto riguarda la sicurezza. In Clang: -Wno-c99-extensions
Daniel O

@DanielW, non si tratta di cosa è meglio o cosa non lo è. questa risposta in base al fatto che l'OP non vuole lo stile A (non c ++), B o C, che copre tutti i casi validi.
iammilind,

@iammilind Penso che un suggerimento sul perché il paradigma mentale di OP sia cattivo potrebbe migliorare la risposta. Lo considero pericoloso come lo è ora.
Daerst,

10

Puoi usare un lambda:

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

Maggiori informazioni su questo idioma sono disponibili sul blog di Herb Sutter .


1
Tale approccio inizializza i campi due volte. Una volta nel costruttore. Il secondo è fb.XXX = YYY.
Dmytro Ovdiienko,

9

Estrarre i concorrenti in funzioni che li descrivono (refactoring di base):

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

So che lo stile è molto vicino a quello che non volevi usare, ma consente una più facile sostituzione dei valori costanti e li spiega anche (quindi non è necessario modificare i commenti), se mai cambiano.

Un'altra cosa che potresti fare (dato che sei pigro) è rendere in linea il costruttore, quindi non devi digitare troppo (rimuovendo "Foobar ::" e il tempo impiegato per passare dal file h al file cpp):

struct FooBar {
  FooBar(int f, float b) : foo(f), bar(b) {}
  int foo;
  float bar;
};

1
Consiglio vivamente a chiunque legga questa domanda di scegliere lo stile nello snippet di codice in basso per questa risposta se tutto ciò che stai cercando di fare è essere in grado di inizializzare rapidamente le strutture con un set di valori.
kayleeFrye_onDeck

8

La tua domanda è alquanto difficile perché anche la funzione:

static FooBar MakeFooBar(int foo, float bar);

può essere chiamato come:

FooBar fb = MakeFooBar(3.4, 5);

a causa delle regole di promozione e conversione per i tipi numerici incorporati. (C non è mai stato davvero fortemente tipizzato)

In C ++, ciò che vuoi è realizzabile, anche se con l'aiuto di modelli e asserzioni statiche:

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, puoi nominare i parametri, ma non otterrai mai di più.

D'altra parte, se tutto ciò che vuoi è denominato parametri, allora scrivi un sacco di codice ingombrante:

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

E se lo desideri puoi aggiungere la protezione promozione del tipo.


1
"In C ++, ciò che vuoi è realizzabile": OP non stava chiedendo di aiutare a evitare il mixup dell'ordine dei parametri? In che modo il modello da te proposto lo raggiungerebbe? Per semplicità, supponiamo di avere 2 parametri, entrambi int.
massimo

@max: lo impedirà solo se i tipi differiscono (anche se sono convertibili tra loro), che è la situazione OP. Se non riesce a distinguere i tipi, ovviamente non funziona, ma questa è un'altra domanda.
Matthieu M.,

Ah capito. Sì, questi sono due problemi diversi, e immagino che il secondo non abbia una buona soluzione in C ++ al momento (ma sembra che C ++ 20 stia aggiungendo il supporto per i nomi dei parametri in stile C99 nell'inizializzazione aggregata).
massimo

6

I frontend C ++ di molti compilatori (inclusi GCC e clang) comprendono la sintassi dell'inizializzatore C. Se puoi, usa semplicemente quel metodo.


16
Che non è conforme allo standard C ++!
Maschera di bit

5
So che non è standard. Ma se puoi usarlo, è ancora il modo più sensato di inizializzare una struttura.
Matthias Urlichs,

2
Puoi proteggere i tipi di xey rendendo privato il costruttore sbagliato:private: FooBar(float x, int y) {};
dmitry_romanov

4
clang (compilatore c ++ basato su llvm) supporta anche questa sintassi. Peccato che non faccia parte dello standard.
nimrodm,

Sappiamo tutti che gli inizializzatori C non fanno parte dello standard C ++. Ma molti compilatori lo capiscono e la domanda non ha detto quale compilatore viene preso di mira, se presente. Quindi, per favore, non sottovalutare questa risposta.
Matthias Urlichs,

4

Ancora un altro modo in C ++ è

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

2
Ingombrante per la programmazione funzionale (ovvero la creazione dell'oggetto nella lista degli argomenti di una chiamata di funzione), ma in realtà un'idea ben diversa altrimenti!
maschera di bit

27
l'ottimizzatore probabilmente lo riduce, ma i miei occhi no.
Matthieu M.

6
Due parole: argh ... argh! In che modo è meglio che utilizzare i dati pubblici con 'Point pt; pt.x = pt.y = 20; `? O se vuoi l'incapsulamento, come è meglio di un costruttore?
OldPeculier,

3
È meglio di un costruttore perché devi guardare la dichiarazione del costruttore per l'ordine dei parametri ... è x, y o y, x ma il modo in cui l'ho mostrato è evidente sul sito di chiamata
parapura rajkumar

2
Questo non funziona se si desidera una const const. o se si desidera dire al compilatore di non consentire strutture non inizializzate. Se vuoi davvero farlo in questo modo, almeno segna i setter con inline!
Matthias Urlichs,

3

Opzione D:

FooBar FooBarMake(int foo, float bar)

C legale, C ++ legale. Facilmente ottimizzabile per i POD. Naturalmente non ci sono argomenti con nome, ma questo è come tutto il C ++. Se vuoi argomenti con nome, l'obiettivo C dovrebbe essere la scelta migliore.

Opzione E:

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

C legale, C ++ legale. Argomenti nominati.


12
Invece di memset che puoi usare FooBar fb = {};in C ++, inizializza di default tutti i membri di struct.
Öö Tiib,

@ ÖöTiib: Purtroppo è illegale C, però.
CB Bailey,

3

So che questa domanda è vecchia, ma c'è un modo per risolverlo fino a quando C ++ 20 porta finalmente questa funzionalità da C a C ++. Quello che puoi fare per risolvere questo è usare le macro del preprocessore con static_asserts per verificare che l'inizializzazione sia valida. (So ​​che le macro sono generalmente sbagliate, ma qui non vedo un altro modo.) Vedi il codice di esempio di seguito:

#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...

Quindi, quando si dispone di una struttura con attributi const, è possibile farlo:

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.);

È un po 'scomodo, perché hai bisogno di macro per ogni possibile numero di attributi e devi ripetere il tipo e il nome della tua istanza nella chiamata macro. Inoltre, non è possibile utilizzare la macro in un'istruzione return, poiché gli assert vengono dopo l'inizializzazione.

Ma risolve il tuo problema: quando cambi la struttura, la chiamata fallirà in fase di compilazione.

Se usi C ++ 17, puoi persino rendere più rigorose queste macro forzando gli stessi tipi, ad esempio:

#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);\

Esiste una proposta C ++ 20 per consentire gli inizializzatori nominati?
Maël Nison,


2

In /* B */C ++ va bene anche il C ++ 0x estenderà la sintassi, quindi è utile anche per i contenitori C ++. Non capisco perché lo chiami cattivo stile?

Se vuoi indicare parametri con nomi, puoi usare la libreria di parametri boost , ma potresti confondere qualcuno che non ha familiarità con esso.

Il riordino dei membri della struttura è come il riordino dei parametri della funzione, tale refactoring può causare problemi se non lo si fa con molta attenzione.


7
Lo chiamo cattivo stile perché penso che sia zero mantenibile. Cosa succede se aggiungo un altro membro tra un anno? O se cambio l'ordine / i tipi dei membri? Ogni pezzo di codice che lo inizializza potrebbe (molto probabilmente) rompersi.
maschera di bit

2
@bitmask Ma fintanto che non hai argomenti nominati, dovresti aggiornare anche le chiamate del costruttore e penso che non molte persone pensano che i costruttori siano cattivi stili non mantenibili. Penso anche che l'inizializzazione denominata non sia C, ma C99, di cui C ++ non è sicuramente un superset.
Christian Rau,

2
Se si aggiunge un altro membro in un anno alla fine della struttura, verrà inizializzato per impostazione predefinita nel codice già esistente. Se li riordini, devi modificare tutto il codice esistente, nulla da fare.
Öö Tiib,

1
@bitmask: anche il primo esempio sarebbe "non mantenibile". Cosa succede se si rinomina invece una variabile nella struttura? Certo, potresti fare un rimpiazzo, ma ciò potrebbe rinominare accidentalmente una variabile che non dovrebbe essere rinominata.
Mike Bailey,

@ChristianRau Da quando C99 non è C? Il gruppo C e C99 non sono una specifica versione / specifica ISO?
Altendky,

1

Che dire di questa sintassi?

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

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

Appena testato su un Microsoft Visual C ++ 2015 e su g ++ 6.0.2. Funziona bene.
È possibile creare una macro specifica anche se si desidera evitare la duplicazione del nome della variabile.


clang++3.5.0-10 con -Weverything -std=c++1zsembra confermarlo. Ma non sembra giusto. Sai dove lo standard conferma che questo è C ++ valido?
Maschera di bit

Non lo so, ma l'ho usato in compilatori diversi da molto tempo fa e non ho riscontrato alcun problema. Ora testato su g ++ 4.4.7 - funziona benissimo.
cls

5
Non penso che questo lavoro. Prova ABCD abc = { abc.b = 7, abc.a = 5 };.
Raymai97,

@deselect, funziona perché il campo è inizializzato con valore, restituito dall'operatore =. Quindi, in realtà inizializzi due volte il membro della classe.
Dmytro Ovdiienko,

1

Per me il modo più pigro per consentire l'inizializzazione in linea è usare questa macro.

#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);

Quella macro crea l'attributo e il metodo di auto-riferimento.

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.