Est-il possible d'empêcher l'omission de membres d'initialisation agrégés?

43

J'ai une structure avec de nombreux membres du même type, comme ceci

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

Le problème est que si j'oublie d'initialiser l'un des membres de la structure (par exemple wasactive), comme ceci:

VariablePointers{activePtr, filename}

Le compilateur ne s'en plaindra pas, mais j'aurai un objet partiellement initialisé. Comment éviter ce genre d'erreur? Je pourrais ajouter un constructeur, mais cela dupliquerait la liste des variables deux fois, donc je dois taper tout cela trois fois!

Veuillez également ajouter des réponses C ++ 11 , s'il existe une solution pour C ++ 11 (actuellement, je suis limité à cette version). Les normes linguistiques plus récentes sont également les bienvenues!

Johannes Schaub - litb
la source
6
Taper un constructeur ne semble pas si terrible. Sauf si vous avez trop de membres, auquel cas, une refactorisation est peut-être nécessaire.
Gonen I
1
@Someprogrammerdude Je pense qu'il veut dire que l'erreur est que vous pouvez accidentellement omettre une valeur d'initialisation
Gonen I
2
@theWiseBro si vous savez comment le tableau / vecteur vous aide, vous devriez poster une réponse. Ce n'est pas si évident, je ne le vois pas
idclev 463035818
2
@Someprogrammerdude Mais est-ce même un avertissement? Impossible de le voir avec VS2019.
acraig5075
8
Il y a un -Wmissing-field-initializersdrapeau de compilation.
Ron

Réponses:

42

Voici une astuce qui déclenche une erreur de l'éditeur de liens si un initialiseur requis est manquant:

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

Usage:

struct Foo {
    int bar = init_required;
};

int main() {
    Foo f;
}

Résultat:

/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

Mises en garde:

  • Avant C ++ 14, cela empêche Foodu tout d'être un agrégat.
  • Cela s'appuie techniquement sur un comportement non défini (violation ODR), mais devrait fonctionner sur n'importe quelle plate-forme saine.
Quentin
la source
Vous pouvez supprimer l'opérateur de conversion, puis c'est une erreur de compilation.
jrok
@jrok oui, mais il en est un dès qu'il Fooest déclaré, même si vous n'appelez jamais réellement l'opérateur.
Quentin
2
@jrok Mais alors, il ne compile pas même si l'initialisation est fournie. godbolt.org/z/yHZNq_ Addendum: Pour MSVC cela fonctionne comme vous l'avez décrit: godbolt.org/z/uQSvDa Est-ce un bug?
n314159
Bien sûr, idiot moi.
jrok
6
Malheureusement, cette astuce ne fonctionne pas avec C ++ 11, car elle deviendra alors un non-agrégat :( J'ai supprimé la balise C ++ 11, donc votre réponse est également viable (veuillez ne pas la supprimer), mais une solution C ++ 11 est toujours préférée, si possible
Johannes Schaub - litb
22

Pour clang et gcc, vous pouvez compiler avec -Werror=missing-field-initializersqui transforme l'avertissement sur les initialiseurs de champ manquants en erreur. Godbolt

Edit: Pour MSVC, il ne semble y avoir aucun avertissement émis même au niveau /Wall, donc je ne pense pas qu'il soit possible d'avertir des initialiseurs manquants avec ce compilateur. Godbolt

n314159
la source
7

Ce n'est pas une solution élégante et pratique, je suppose ... mais devrait également fonctionner avec C ++ 11 et donner une erreur de compilation (pas de liaison).

L'idée est d'ajouter dans votre structure un membre supplémentaire, en dernière position, d'un type sans initialisation par défaut (et qui ne peut pas s'initialiser avec une valeur de type VariablePtr(ou quel que soit le type des valeurs précédentes)

Par exemple

struct bar
 {
   bar () = delete;

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

   bar (int) 
    { }
 };

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

   bar sentinel;
 };

De cette façon, vous êtes obligé d'ajouter tous les éléments dans votre liste d'initialisation agrégée, inclus la valeur pour initialiser explicitement la dernière valeur (un entier pour sentinel, dans l'exemple) ou vous obtenez un "appel au constructeur supprimé de 'bar'".

Donc

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

compiler et

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

non.

Malheureusement aussi

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

ne compile pas.

-- ÉDITER --

Comme indiqué par MSalters (merci), il y a un défaut (un autre défaut) dans mon exemple d'origine: une barvaleur pourrait être initialisée avec une charvaleur (qui est convertible en int), donc fonctionne l'initialisation suivante

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

et cela peut être très déroutant.

Pour éviter ce problème, j'ai ajouté le constructeur de modèle supprimé suivant

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

de sorte que la f4déclaration précédente donne une erreur de compilation car la dvaleur est interceptée par le constructeur de modèle qui est supprimé

max66
la source
Merci, c'est sympa! Ce n'est pas parfait comme vous l'avez mentionné, et rend également la foo f;compilation impossible, mais c'est peut-être plus une fonctionnalité qu'un défaut de cette astuce. Acceptera s'il n'y a pas de meilleure proposition que celle-ci.
Johannes Schaub - litb
1
Je voudrais que le constructeur de barres accepte un membre de classe imbriqué const appelé quelque chose comme init_list_end pour plus de lisibilité
Gonen I
@GonenI - pour plus de lisibilité, vous pouvez accepter un enum, et nommer init_list_end(o simplement list_end) une valeur de cela enum; mais la lisibilité ajoute beaucoup de dactylographie, donc, étant donné que la valeur supplémentaire est le point faible de cette réponse, je ne sais pas si c'est une bonne idée.
max66
Peut-être ajouter quelque chose comme constexpr static int eol = 0;dans l'en-tête de bar. test{a, b, c, eol}me semble assez lisible.
n314159
@ n314159 - eh bien ... devenez bar::eol; c'est presque comme passer une enumvaleur; mais je ne pense pas que ce soit important: le cœur de la réponse est "ajoutez dans votre structure un membre supplémentaire, en dernière position, d'un type sans initialisation par défaut"; la barpartie est juste un exemple trivial pour montrer que la solution fonctionne; le "type sans initialisation par défaut" exact devrait dépendre des circonstances (à mon humble avis).
max66
4

Pour CppCoreCheck, il existe une règle pour vérifier exactement cela, si tous les membres ont été initialisés et qui peuvent être transformés d'avertissement en erreur - cela est bien sûr à l'échelle du programme.

Mise à jour:

La règle que vous souhaitez vérifier fait partie de la sécurité des types Type.6:

Type.6: Toujours initialiser une variable membre: toujours initialiser, éventuellement en utilisant des constructeurs par défaut ou des initialiseurs de membres par défaut.

Darune
la source
2

Le moyen le plus simple est de ne pas donner au type des membres un constructeur sans argument:

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 

Autre option: si vos membres sont const &, vous devez tous les initialiser:

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

Si vous pouvez vivre avec un membre et un constable factice, vous pouvez combiner cela avec l'idée de @ max66 d'une sentinelle.

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

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

Si le nombre de clauses d'initialisation est inférieur au nombre de membres ou que la liste d'initialisation est complètement vide, les membres restants sont initialisés en valeur. Si un membre d'un type de référence est l'un de ces membres restants, le programme est mal formé.

Une autre option consiste à prendre l'idée sentinelle de max66 et à ajouter du sucre syntaxique pour plus de lisibilité

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
la source
Malheureusement, cela rend Ainamovible et change la copie sémantique ( An'est plus un agrégat de valeurs, pour ainsi dire) :(
Johannes Schaub - litb
@ JohannesSchaub-litb OK. Que diriez-vous de cette idée dans ma réponse modifiée?
Gonen I
@ JohannesSchaub-litb: tout aussi important, la première version ajoute un niveau d'indirection en faisant pointer les membres. Plus important encore, ils doivent être une référence à quelque chose, et les 1,2,3objets sont en fait des locaux dans le stockage automatique qui sortent du cadre lorsque la fonction se termine. Et cela fait la taille de (A) 24 au lieu de 3 sur un système avec des pointeurs 64 bits (comme x86-64).
Peter Cordes
Une référence factice augmente la taille de 3 à 16 octets (remplissage pour l'alignement du membre du pointeur (référence) + le pointeur lui-même.) Tant que vous n'utilisez jamais la référence, c'est probablement correct si elle pointe vers un objet qui est sorti de portée. Je m'inquiéterais certainement de ne pas l'optimiser et de ne pas le copier. (Une classe vide a de meilleures chances de s'optimiser en dehors de sa taille, donc la troisième option ici est la moins mauvaise, mais elle coûte toujours de l'espace dans chaque objet au moins dans certains ABI. optimisation dans certains cas.)
Peter Cordes