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

33

La société dans laquelle je travaille initialise toutes ses structures de données à l’aide d’une fonction d’initialisation comme celle-ci:

//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 eu du mal à essayer 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 at-il un avantage à l’un sur l’autre?
Lequel devrais-je faire et comment pourrais-je le justifier en tant que meilleure pratique?

Trevor Hickey
la source
4
@gnat Ceci est une question explicite sur l'initialisation de la structure. Ce fil incarne les mêmes raisons que je souhaiterais voir appliquées à cette décision de conception particulière.
Trevor Hickey
2
@ Jefffrey Nous sommes en C, nous ne pouvons donc pas avoir de méthodes. Ce n'est pas toujours un ensemble direct de valeurs non plus. Parfois, initialiser une structure consiste à obtenir des valeurs (en quelque sorte) et effectue une certaine logique pour initialiser la structure.
Trevor Hickey
1
@JacquesB J'ai "Chaque composant que vous construisez sera différent des autres. Il existe une fonction Initialize () utilisée ailleurs pour la structure. Techniquement, l'appeler un constructeur est trompeur."
Trevor Hickey
1
@TrevorHickey InitializeFoo()est un constructeur. La seule différence par rapport à un constructeur C ++ est que le thispointeur est passé explicitement plutôt qu'implicitement. Le code compilé de InitializeFoo()et un C ++ correspondant Foo::Foo()sont exactement les mêmes.
cmaster
2
Meilleure option: arrêtez d’utiliser C sur C ++. Autowin.
Thomas Eding

Réponses:

25

Dans la deuxième approche, vous n'aurez jamais un Foo semi-initialisé. Mettre toute la construction au même endroit semble un endroit plus sensé et plus é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 1ère voie, soit une injection de constructeur comme la 2e) . Ni est faux.

Donc, si aucun des deux n’est faux et que le reste de la société utilise l’approche n ° 1, vous devriez alors vous adapter au code existant et ne pas essayer de le gâcher en introduisant un nouveau modèle. C’est vraiment le facteur le plus important en jeu ici, jouez bien avec vos nouveaux amis et n’essayez pas d’être ce flocon de neige spécial qui fait les choses différemment.

gbjbaanb
la source
Ok, semble raisonnable. J'avais l'impression qu'initialiser un objet sans pouvoir voir quel type d'entrée l'initialisait était source de confusion. J'essayais de suivre le concept de données entrées / données pour produire un code prévisible et testable. Faire le contraire a semblé augmenter le couplage car le fichier source de ma structure nécessitait des dépendances supplémentaires pour effectuer l'initialisation. Vous avez cependant raison, en ce sens que je ne veux pas faire chavirer le bateau à moins qu'une voie soit hautement préférée par une autre.
Trevor Hickey
4
@TrevorHickey: En fait, je dirais qu'il y a deux différences essentielles entre les exemples que vous donnez - (1) Dans l'un, la fonction reçoit un pointeur sur la structure à initialiser et dans l'autre, elle renvoie une structure initialisée; (2) Dans l'un, les paramètres d'initialisation sont transmis à la fonction et dans l'autre, ils sont implicites. Vous semblez en demander davantage sur (2), mais les réponses ici se concentrent sur (1). Vous voudrez peut-être préciser que - je soupçonne la plupart des gens de recommander un hybride des deux en utilisant des paramètres explicites et un pointeur:void SetupFoo(Foo *out, int a, int b, int c)
psmears le
1
Comment la première approche conduirait-elle à une Foostructure "semi-initialisée" ? La première approche effectue également toutes les initialisations en un seul endroit. (Ou envisagez-vous qu'une structure non initialisée Foosoit "semi-initialisée"?)
jamesdlin
1
@jamesdlin dans les cas où le Foo est créé et que le InitialiseFoo est accidentellement oublié. C’était juste une figure de parler de décrire l’initialisation en 2 phases sans taper une longue description. Je pensais que les développeurs expérimentés comprendraient.
gbjbaanb
22

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

Cependant, la deuxième 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é dans l'objet final. C'est pourquoi je verrais la deuxième approche légèrement inférieure. Le refoulement que vous avez reçu est probablement dû à cette copie superflue.

    Cela est encore pire lorsque vous dérivez une classe Derivedde Foo(struct sont largement utilisés pour l' orientation de l' objet en C): Avec la seconde approche, la fonction ConstructDerived()appellerait ConstructFoo(), copiez le temporaire résultant Fooobjet sur dans la fente de superclasse d'un Derivedobjet; terminer l'initialisation de l' Derivedobjet; 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 ConstructClass()fonctions n’ont pas accès à l’adresse de l’objet en construction. Cela rend impossible la liaison d'objets pendant la construction, car cela est nécessaire lorsqu'un objet doit s'inscrire lui-même avec un autre objet pour un rappel.


Enfin, tous ne structssont pas des classes à part entière. Certains structsregroupent efficacement un ensemble de variables, sans aucune restriction interne aux valeurs de ces variables. typedef struct Point { int x, y; } Point;serait un bon exemple de cela. Pour ceux-ci, l'utilisation d'une fonction d'initialisation semble excessive. 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 };
}
cmaster
la source
5
Je ne pensais pas que les copies seraient un problème à cause de en.wikipedia.org/wiki/Copy_elision
Trevor Hickey
5
Le fait que le compilateur puisse supprimer la copie n'atténue pas le fait que vous avez écrit la copie. En C, écrire des opérations superflues et s’appuyer sur le compilateur pour les corriger est considéré comme mauvais. Cela diffère du C ++ où les gens sont fiers de prouver que le compilateur peut théoriquement supprimer tout le fouillis laissé par leurs modèles imbriqués. En C, les gens essaient d'écrire exactement le code que la machine doit exécuter. Quoi qu'il en soit, le point concernant les adresses inaccessibles reste, la copie élision ne peut pas vous aider là.
cmaster
3
Toute personne utilisant un compilateur doit s’attendre à ce que le code qu’il écrit soit transformé par le compilateur. Sauf s’ils utilisent un interpréteur C matériel, le code qu’ils écrivent ne sera pas le code qu’ils exécuteront, même s’il est facile de croire le contraire. S'ils comprennent leur compilateur, ils comprendront élision, et cela n'est pas différent de int x = 3;ne pas stocker la chaîne xdans le binaire. L'adresse et les points d'héritage sont bons; l'échec présumé de l'élision est stupide.
Yakk
@Yakk: Historiquement, C a été inventé pour servir de langage d'assemblage de haut niveau pour la programmation système. Depuis lors, son identité est devenue de plus en plus trouble. Certaines personnes veulent que ce soit un langage d’application optimisé, mais comme aucune meilleure forme de langage d’assemblage de haut niveau n’a émergé, C est toujours nécessaire pour remplir ce dernier rôle. Je ne vois aucun inconvénient à l'idée qu'un code de programme bien écrit devrait se comporter de manière décente, même lorsqu'il est compilé avec des optimisations minimales, bien que pour que cela fonctionne réellement, il faudrait que C ajoute certaines choses qui lui manquaient depuis longtemps.
Supercat
@Yakk: Avoir des directives, par exemple, qui diraient au compilateur "Les variables suivantes peuvent être conservées dans des registres en toute sécurité pendant la portion de code suivante", ainsi qu'un moyen de copier par bloc un type autre que celui unsigned charqui permettrait des optimisations pour lesquelles Une règle de crénelage stricte serait insuffisante, tout en clarifiant les attentes du programmeur.
Supercat
1

En fonction du contenu de la structure et du compilateur 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 restituées dans des registres; pour les fonctions renvoyant d'autres types de structure, l'appelant doit allouer de l'espace pour la structure temporaire (généralement sur la pile) et transmettre son adresse en tant que paramètre "masqué"; dans les cas où le retour d'une fonction est stocké directement dans une variable locale dont l'adresse n'est pas détenue par un code extérieur, certains compilateurs peuvent être en mesure de transmettre directement l'adresse de cette variable.

Si un type de structure satisfait aux exigences d'une mise en oeuvre particulière pour être renvoyé dans des registres (ne dépassant pas un mot machine, ou remplissant deux mots machine précisément) ayant un retour de fonction, la structure peut être plus rapide que de passer l'adresse d'une structure, en particulier car exposer l'adresse d'une variable à un code extérieur susceptible de conserver une copie de celle-ci pourrait empêcher certaines optimisations utiles. Si un type ne satisfait pas à ces exigences, le code généré pour une fonction qui retourne une structure sera similaire à celui d'une fonction qui accepte un pointeur de destination. le code appelant serait probablement plus rapide pour le formulaire utilisant un pointeur, mais ce formulaire perdrait certaines opportunités d'optimisation.

C’est dommage que C ne fournisse pas le moyen de dire qu’il est interdit à une fonction de conserver une copie d’un pointeur passé (une sémantique semblable à une référence C ++), car le passage d’un pointeur aussi restreint serait un avantage direct en termes de performances. un pointeur sur un objet préexistant, tout en évitant les coûts sémantiques liés à l'obligation pour un compilateur de considérer l'adresse d'une variable comme "exposée".

supercat
la source
3
Dernier point: rien dans C ++ n'empêche une fonction de conserver une copie d'un pointeur transmis en tant que référence, la fonction peut simplement prendre l'adresse de l'objet. Il est également libre d'utiliser la référence pour construire un autre objet contenant cette référence (aucun pointeur nu créé). Néanmoins, la copie du pointeur ou la référence dans l'objet peut survivre à l'objet sur lequel ils pointent, créant un pointeur / une référence en suspens. Donc, le point sur la sécurité de référence est assez muet.
cmaster
@cmaster: sur les plates-formes qui renvoient des structures en passant un pointeur sur la mémoire de stockage temporaire, les compilateurs C ne fournissent aucune fonction appelée avec aucun moyen d'accéder à l'adresse de cette mémoire. En C ++, il est possible d'obtenir l'adresse d'une variable transmise par référence, mais à moins que l'appelant ne garantisse la durée de vie de l'élément transmis (dans ce cas, il aurait généralement passé un pointeur). Un comportement indéfini sera probablement généré.
Supercat
1

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

Si vous considérez un ensemble de structures associées, si l'initialisation peut échouer pour l'une d'entre elles, il peut être intéressant que toutes utilisent le style de paramètre sortant pour des raisons de cohérence.

Viktor Dahl
la source
1
Bien que l'on puisse juste définir errno.
Déduplicateur
0

Je présume que vous vous concentrez sur l'initialisation via le paramètre de sortie plutôt que sur l'initialisation via le retour, et non sur la divergence dans la manière dont les arguments de construction sont fournis.

Notez que la première approche pourrait permettre Food’être opaque (mais pas avec la manière dont vous l’utilisez actuellement), ce qui est généralement souhaitable pour la maintenabilité à long terme. Vous pouvez par exemple envisager une fonction qui alloue une Foostructure opaque sans l'initialiser. Ou peut-être avez-vous besoin de réinitialiser une Foostructure qui avait été précédemment initialisée avec différentes valeurs.

Jamesdlin
la source
Downvoter, vous voulez expliquer? Est-ce que quelque chose que j'ai dit factuellement incorrect?
jamesdlin