web-dev-qa-db-fra.com

Dois-je initialiser les structures C via un paramètre ou une valeur de retour?

L'entreprise dans laquelle je travaille initialise toutes ses structures de données via une fonction d'initialisation comme ceci:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
InitializeFoo(Foo* const foo){
   foo->a = x; //derived here based on other data
   foo->b = y; //derived here based on other data
   foo->c = z; //derived here based on other data
}

//initializing the structure  
Foo foo;
InitializeFoo(&foo);

J'ai obtenu quelques Push en essayant d'initialiser mes structures comme ceci:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
Foo ConstructFoo(int a, int b, int c){
   Foo foo;
   foo.a = a; //part of parameter input (inputs derived outside of function)
   foo.b = b; //part of parameter input (inputs derived outside of function)
   foo.c = c; //part of parameter input (inputs derived outside of function)
   return foo;
}

//initialize (or construct) the structure
Foo foo = ConstructFoo(x,y,z);

Y a-t-il un avantage l'un par rapport à l'autre?
Laquelle dois-je faire et comment pourrais-je la justifier en tant que meilleure pratique?

34
Trevor Hickey

Dans la 2ème approche, vous n'aurez jamais de Foo à moitié initialisé. Mettre toute la construction en un seul endroit semble être un endroit plus sensé et évident.

Mais ... la 1ère voie n'est pas si mauvaise, et est souvent utilisée dans de nombreux domaines (il y a même une discussion sur la meilleure façon d'injecter une dépendance, soit une injection de propriété comme votre première voie, soit une injection de constructeur comme la 2e) . Ni est faux.

Donc, si ni l'un ni l'autre ne se trompe et le reste de l'entreprise utilise l'approche n ° 1, vous devez vous adapter à la base de code existante et ne pas essayer de la gâcher en introduisant un nouveau modèle. C'est vraiment le facteur le plus important ici, jouez à Nice avec vos nouveaux amis et n'essayez pas d'être ce flocon de neige spécial qui fait les choses différemment.

25
gbjbaanb

Les deux approches regroupent le code d'initialisation en un seul appel de fonction. Jusqu'ici tout va bien.

Cependant, la seconde approche pose deux problèmes:

  1. Le second ne construit pas réellement l'objet résultant, il initialise un autre objet sur la pile, qui est ensuite copié sur l'objet final. C'est pourquoi je considérerais la deuxième approche comme légèrement inférieure. Le refoulement que vous avez reçu est probablement dû à cette copie superflue.

    C'est encore pire lorsque vous dérivez une classe Derived de Foo (les structures sont largement utilisées pour l'orientation des objets en C): avec la deuxième approche, la fonction ConstructDerived() appelle ConstructFoo(), copiez l'objet Foo temporaire résultant dans l'emplacement de superclasse d'un objet Derived; terminer l'initialisation de l'objet Derived; seulement pour que l'objet résultant soit recopié à son retour. Ajoutez une troisième couche, et le tout devient complètement ridicule.

  2. Avec la seconde approche, les fonctions ConstructClass() n'ont pas accès à l'adresse de l'objet en construction. Cela rend impossible de lier des objets pendant la construction, car cela est nécessaire lorsqu'un objet doit s'enregistrer auprès d'un autre objet pour un rappel.


Enfin, toutes les structs ne sont pas des classes à part entière. Certains structs regroupent simplement un ensemble de variables sans aucune restriction interne aux valeurs de ces variables. typedef struct Point { int x, y; } Point; en serait un bon exemple. Pour ceux-ci, l'utilisation d'une fonction d'initialisation semble exagérée. Dans ces cas, la syntaxe littérale composée peut être pratique (c'est C99):

Point = { .x = 7, .y = 9 };

ou

Point foo(...) {
    //other stuff

    return (Point){ .x = n, .y = n*n };
}

Je suppose que vous vous concentrez sur l'initialisation via le paramètre de sortie par rapport à l'initialisation via le retour, pas sur la différence dans la façon dont les arguments de construction sont fournis.

Notez que la première approche pourrait permettre à Foo d'être opaque (mais pas avec la façon dont vous l'utilisez actuellement), et c'est généralement souhaitable pour une maintenabilité à long terme. Vous pouvez considérer, par exemple, une fonction qui alloue une structure Foo opaque sans l'initialiser. Ou peut-être devez-vous réinitialiser une structure Foo qui a été précédemment initialisée avec des valeurs différentes.

1
jamesdlin

Selon le contenu de la structure et le compilateur particulier utilisé, l'une ou l'autre approche pourrait être plus rapide. Un schéma typique est que les structures répondant à certains critères peuvent être renvoyées dans des registres; pour les fonctions renvoyant d'autres types de structure, l'appelant doit allouer de l'espace pour la structure temporaire quelque part (généralement sur la pile) et transmettre son adresse en tant que paramètre "caché"; dans les cas où le retour d'une fonction est stocké directement dans une variable locale dont l'adresse n'est détenue par aucun code extérieur, certains compilateurs peuvent être en mesure de transmettre directement l'adresse de cette variable.

Si un type de structure satisfait les exigences d'une implémentation particulière pour être retourné dans les registres (par exemple, être ni plus grand qu'un mot machine, ou remplir précisément deux mots machine) ayant une fonction renvoyer, la structure peut être plus rapide que de passer l'adresse d'une structure, en particulier car exposer l'adresse d'une variable à du code extérieur qui pourrait en conserver une copie pourrait empêcher certaines optimisations utiles. Si un type ne satisfait pas à ces exigences, le code généré pour une fonction qui renvoie une structure sera similaire à celui d'une fonction qui accepte un pointeur de destination; le code appelant serait probablement plus rapide pour le formulaire qui prend un pointeur, mais ce formulaire perdrait certaines opportunités d'optimisation.

Il est dommage que C ne fournisse pas un moyen de dire qu'une fonction est interdite de conserver une copie d'un pointeur transmis (sémantique similaire à une référence C++), car le passage d'un tel pointeur restreint aurait les avantages directs en termes de performances de passer un pointeur vers un objet préexistant, mais en même temps éviter les coûts sémantiques d'exiger qu'un compilateur considère l'adresse d'une variable "exposée".

1
supercat

Un argument en faveur du style "paramètre de sortie" est qu'il permet à la fonction de renvoyer un code d'erreur.

struct MyStruct {
    int x;
    char *y;
    // ...
};

int MyStruct_init(struct MyStruct *out) {
    // ...
    char *c = malloc(n);
    if (!c) {
        return -1;
    }
    out->y = c;
    return 0;  // Success!
}

En considérant un ensemble de structures connexes, si l'initialisation peut échouer pour l'une d'entre elles, il peut être utile de les utiliser toutes par souci de cohérence.

1
Viktor Dahl