Initialisation de structure C ++ pratique

141

J'essaie de trouver un moyen pratique d'initialiser les structures C ++ 'pod'. Maintenant, considérez la structure suivante:

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

Si je veux initialiser commodément ceci en C (!), Je pourrais simplement écrire:

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

Notez que je veux éviter explicitement la notation suivante, car elle me semble être faite pour me casser le cou si je change quoi que ce soit dans la structure à l'avenir:

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

Pour obtenir le même (ou au moins similaire) en C ++ que dans l' /* A */exemple, je devrais implémenter un constructeur idiot:

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

Ce qui est bon pour faire bouillir de l'eau, mais ne convient pas aux paresseux (la paresse est une bonne chose, non?). En outre, il est à peu près aussi mauvais que l' /* B */exemple, car il n'indique pas explicitement quelle valeur va à quel membre.

Donc, ma question est essentiellement de savoir comment je peux réaliser quelque chose de similaire /* A */ou meilleur en C ++? Sinon, je serais d'accord avec une explication pourquoi je ne devrais pas vouloir faire cela (c'est-à-dire pourquoi mon paradigme mental est mauvais).

ÉDITER

Par pratique , je veux dire également maintenable et non redondant .

masque binaire
la source
2
Je pense que l'exemple B est aussi proche que vous allez l'être.
Marlon
2
Je ne vois pas en quoi l'exemple B est «mauvais style». Cela a du sens pour moi, puisque vous initialisez chaque membre à tour de rôle avec leurs valeurs respectives.
Mike Bailey
26
Mike, c'est un mauvais style car on ne sait pas quelle valeur va à quel membre. Vous devez aller regarder la définition de la structure, puis compter les membres pour trouver ce que chaque valeur signifie.
jnnnnn
9
De plus, si la définition de FooBar devait changer dans le futur, l'initialisation pourrait être interrompue.
Edward Falk
si l'initialisation devient longue et complexe, n'oubliez pas le modèle du constructeur
traîneau

Réponses:

20

Les initialisations désignées seront prises en charge en c ++ 2a, mais vous n'avez pas à attendre, car elles sont officiellement prises en charge par GCC, Clang et 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

Ivaigult
la source
Attention emptor: gardez à l'esprit que si vous ajoutez des paramètres à la fin de la structure plus tard, les anciennes initialisations seront toujours compilées silencieusement sans avoir été initialisées.
Catskul
1
@Catskul Non. Il sera initialisé avec une liste d'initialisation vide, ce qui entraînera une initialisation à zéro.
ivaigult
Vous avez raison. Je vous remercie. Je devrais clarifier, les paramètres restants seront automatiquement initialisés par défaut. Le point que je voulais dire était que quiconque espère que cela pourrait aider à appliquer une initialisation explicite complète des types de POD sera déçu.
Catskul
43

Puisque style An'est pas autorisé en C ++ et que vous ne voulez pas, que style Bdiriez-vous d'utiliser style BX:

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

Au moins aider dans une certaine mesure.

iammilind
la source
8
+1: cela n'assure pas vraiment une initialisation correcte (à partir du POV du compilateur) mais aide bien sûr le lecteur ... bien que les commentaires doivent être synchronisés.
Matthieu M.
18
Le commentaire n'empêche pas l'initialisation de la structure d'être interrompue si j'insère un nouveau champ entre fooet bardans le futur. C initialiserait toujours les champs souhaités, mais pas C ++. Et c'est le point de la question - comment obtenir le même résultat en C ++. Je veux dire, Python le fait avec des arguments nommés, C - avec des champs «nommés», et C ++ devrait avoir quelque chose aussi, j'espère.
dmitry_romanov
2
Commentaires synchronisés? Laisse-moi tranquille. La sécurité passe par la fenêtre. Réorganisez les paramètres et la rampe. Beaucoup mieux avec explicit FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) . Notez le mot-clé explicite . Même briser la norme est meilleur en matière de sécurité. Dans Clang: -Wno-c99-extensions
Daniel O
@DanielW, il ne s'agit pas de ce qui est meilleur ou de ce qui ne l'est pas. cette réponse en accord avec le fait que l'OP ne veut pas de Style A (pas c ++), B ou C, qui couvre tous les cas valides.
iammilind
@iammilind Je pense qu'un indice sur la raison pour laquelle le paradigme mental d'OP est mauvais pourrait améliorer la réponse. Je considère que c'est dangereux tel qu'il est maintenant.
Daerst
10

Vous pouvez utiliser un lambda:

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

Plus d'informations sur cet idiome peuvent être trouvées sur le blog de Herb Sutter .

cil
la source
1
Une telle approche initialise les champs deux fois. Une fois dans le constructeur. Le deuxième est fb.XXX = YYY.
Dmytro Ovdiienko
9

Extraire les contants dans des fonctions qui les décrivent (refactoring de base):

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

Je sais que le style est très proche de celui que vous ne vouliez pas utiliser, mais il permet de remplacer plus facilement les valeurs constantes et de les expliquer également (donc pas besoin de modifier les commentaires), si jamais ils changent.

Une autre chose que vous pouvez faire (puisque vous êtes paresseux) est de rendre le constructeur en ligne, pour ne pas avoir à taper autant (en supprimant "Foobar ::" et le temps passé à basculer entre les fichiers h et cpp):

struct FooBar {
  FooBar(int f, float b) : foo(f), bar(b) {}
  int foo;
  float bar;
};
ralphtheninja
la source
1
Je recommande fortement à quiconque lisant cette question de choisir le style dans l'extrait de code du bas pour cette réponse si tout ce que vous cherchez à faire est de pouvoir initialiser rapidement les structures avec un ensemble de valeurs.
kayleeFrye_onDeck
8

Votre question est un peu difficile car même la fonction:

static FooBar MakeFooBar(int foo, float bar);

peut être appelé comme:

FooBar fb = MakeFooBar(3.4, 5);

en raison des règles de promotion et de conversion pour les types numériques intégrés. (C n'a jamais été vraiment fortement typé)

En C ++, ce que vous voulez est réalisable, mais avec l'aide de modèles et d'assertions statiques:

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

En C, vous pouvez nommer les paramètres, mais vous n'irez jamais plus loin.

D'un autre côté, si vous ne voulez que des paramètres nommés, vous écrivez beaucoup de code encombrant:

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

Et vous pouvez ajouter une protection de promotion de type si vous le souhaitez.

Matthieu M.
la source
1
"En C ++, ce que vous voulez est réalisable": OP n'a-t-il pas demandé de contribuer à empêcher la confusion de l'ordre des paramètres? Comment le modèle que vous proposez permet-il d'y parvenir? Pour simplifier, disons que nous avons 2 paramètres, tous deux int.
max
@max: Cela ne l'empêchera que si les types diffèrent (même s'ils sont convertibles les uns aux autres), ce qui est la situation OP. S'il ne peut pas distinguer les types, alors bien sûr cela ne fonctionne pas, mais c'est une toute autre question.
Matthieu M.
Ah compris. Oui, ce sont deux problèmes différents, et je suppose que le second n'a pas de bonne solution en C ++ pour le moment (mais il semble que C ++ 20 ajoute la prise en charge des noms de paramètres de style C99 dans l'initialisation de l'agrégat).
max
6

Les interfaces C ++ de nombreux compilateurs (y compris GCC et clang) comprennent la syntaxe de l'initialiseur C. Si vous le pouvez, utilisez simplement cette méthode.

Matthias Urlichs
la source
16
Ce qui n'est pas conforme à la norme C ++!
bitmask
5
Je sais que ce n'est pas standard. Mais si vous pouvez l'utiliser, c'est toujours le moyen le plus judicieux d'initialiser une structure.
Matthias Urlichs
2
Vous pouvez protéger les types de x et y en rendant le mauvais constructeur privé:private: FooBar(float x, int y) {};
dmitry_romanov
4
clang (compilateur C ++ basé sur llvm) prend également en charge cette syntaxe. Dommage que cela ne fasse pas partie de la norme.
nimrodm
Nous savons tous que les initialiseurs C ne font pas partie du standard C ++. Mais de nombreux compilateurs le comprennent et la question ne dit pas quel compilateur est ciblé, le cas échéant. Donc, s'il vous plaît, ne rejetez pas cette réponse.
Matthias Urlichs
4

Encore une autre façon en C ++ est

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
la source
2
Lourd pour la programmation fonctionnelle (c'est-à-dire créer l'objet dans la liste d'arguments d'un appel de fonction), mais vraiment une bonne idée sinon!
bitmask
27
l'optimiseur le réduit probablement, mais pas mes yeux.
Matthieu M.
6
Deux mots: argh ... argh! En quoi est-ce mieux que d'utiliser des données publiques avec 'Point pt; pt.x = pt.y = 20; `? Ou si vous voulez une encapsulation, en quoi est-ce meilleur qu'un constructeur?
OldPeculier
3
C'est mieux qu'un constructeur parce que vous devez regarder la déclaration du constructeur pour l'ordre des paramètres ... est-ce x, y ou y, x mais la façon dont je l'ai montré est évidente sur le site d'appel
parapura rajkumar
2
Cela ne fonctionne pas si vous voulez une structure const. ou si vous voulez dire au compilateur de ne pas autoriser les structures non initialisées. Si vous voulez vraiment le faire de cette façon, marquez au moins les setters avec inline!
Matthias Urlichs
3

Option D:

FooBar FooBarMake(int foo, float bar)

C juridique, C ++ juridique. Facilement optimisable pour les POD. Bien sûr, il n'y a pas d'arguments nommés, mais c'est comme tout C ++. Si vous voulez des arguments nommés, Objective C devrait être un meilleur choix.

Option E:

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

C juridique, C ++ juridique. Arguments nommés.

John
la source
12
Au lieu de memset que vous pouvez utiliser FooBar fb = {};en C ++, il initialise par défaut tous les membres de la structure.
Öö Tiib
@ ÖöTiib: Malheureusement, c'est un C illégal.
CB Bailey
3

Je sais que cette question est ancienne, mais il existe un moyen de résoudre cela jusqu'à ce que C ++ 20 apporte enfin cette fonctionnalité du C au C ++. Ce que vous pouvez faire pour résoudre ce problème, c'est utiliser des macros de préprocesseur avec static_asserts pour vérifier que votre initialisation est valide. (Je sais que les macros sont généralement mauvaises, mais ici je ne vois pas d'autre moyen.) Voir l'exemple de code ci-dessous:

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

Ensuite, lorsque vous avez une structure avec des attributs const, vous pouvez le faire:

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

C'est un peu gênant, car vous avez besoin de macros pour chaque nombre possible d'attributs et vous devez répéter le type et le nom de votre instance dans l'appel de macro. Vous ne pouvez pas non plus utiliser la macro dans une instruction return, car les assertions viennent après l'initialisation.

Mais cela résout votre problème: lorsque vous modifiez la structure, l'appel échouera au moment de la compilation.

Si vous utilisez C ++ 17, vous pouvez même rendre ces macros plus strictes en forçant les mêmes types, par exemple:

#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
la source
Existe-t-il une proposition C ++ 20 pour autoriser les initialiseurs nommés?
Maël Nison
1
@ MaëlNison Oui: initialiseurs désignés (depuis C ++ 20)
Max Vollmer
2

Le chemin /* B */ est correcte en C ++ et le C ++ 0x va étendre la syntaxe, donc il est également utile pour les conteneurs C ++. Je ne comprends pas pourquoi vous appelez ça du mauvais style?

Si vous souhaitez indiquer des paramètres avec des noms, vous pouvez utiliser la bibliothèque de paramètres boost , mais cela peut dérouter quelqu'un qui ne le connaît pas.

La réorganisation des membres de structure est comme la réorganisation des paramètres de fonction, une telle refactorisation peut poser des problèmes si vous ne le faites pas très soigneusement.

Öö Tiib
la source
7
Je l'appelle mauvais style parce que je pense que c'est zéro maintenable. Et si j'ajoute un autre membre dans un an? Ou si je change l'ordre / les types des membres? Chaque morceau de code qui l'initialise peut (très probablement) casser.
bitmask
2
@bitmask Mais tant que vous n'avez pas d'arguments nommés, vous devrez également mettre à jour les appels aux constructeurs et je pense que peu de gens pensent que les constructeurs sont du mauvais style impossible à maintenir. Je pense également que l'initialisation nommée n'est pas C, mais C99, dont C ++ n'est certainement pas un sur-ensemble.
Christian Rau
2
Si vous ajoutez un autre membre dans un an à la fin de la structure, il sera initialisé par défaut dans le code déjà existant. Si vous les réorganisez, vous devez modifier tout le code existant, rien à faire.
Öö Tiib
1
@bitmask: Le premier exemple serait alors "non maintenable" également. Que se passe-t-il si vous renommez une variable dans la structure à la place? Bien sûr, vous pouvez faire un remplacement tout, mais cela pourrait renommer accidentellement une variable qui ne devrait pas être renommée.
Mike Bailey
@ChristianRau Depuis quand C99 n'est-il pas C? N'est-ce pas C le groupe et C99 une version / spécification ISO particulière?
altendky
1

Et cette syntaxe?

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

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

Juste testé sur un Microsoft Visual C ++ 2015 et sur g ++ 6.0.2. Fonctionne bien.
Vous pouvez également créer une macro spécifique si vous souhaitez éviter de dupliquer le nom de la variable.

cls
la source
clang++3.5.0-10 avec -Weverything -std=c++1zsemble le confirmer. Mais ça n'a pas l'air bien. Savez-vous où la norme confirme qu'il s'agit d'un C ++ valide?
bitmask
Je ne sais pas, mais je l'ai utilisé dans différents compilateurs depuis longtemps et je n'ai vu aucun problème. Maintenant testé sur g ++ 4.4.7 - fonctionne très bien.
cls
5
Je ne pense pas que cela fonctionne. Essayez ABCD abc = { abc.b = 7, abc.a = 5 };.
raymai97
@deselect, cela fonctionne car le champ est initialisé avec la valeur, renvoyée par l'opérateur =. Donc, en fait, vous initialisez deux fois le membre de classe.
Dmytro Ovdiienko
1

Pour moi, le moyen le plus paresseux d'autoriser l'initialisation en ligne est d'utiliser cette 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);

Cette macro crée un attribut et une méthode d'auto-référence.

Emanuele Pavanello
la source