En C, comment choisirais-je de retourner une structure ou un pointeur sur une structure?

Travailler sur mon muscle C ces derniers temps et regarder à travers les nombreuses bibliothèques avec lesquelles j’ai travaillé m’a certainement donné une bonne idée de ce qui est une bonne pratique. Une chose que je n’ai PAS vue est une fonction qui retourne une structure:

something_t make_something() { ... } 

De ce que j’ai absorbé, c’est la “bonne” façon de procéder:

 something_t *make_something() { ... } void destroy_something(something_t *object) { ... } 

L’architecture dans l’extrait de code 2 est plus populaire que l’extrait 1. Donc, maintenant, je me demande pourquoi renvoyer directement une structure, comme dans l’extrait 1? Quelles différences dois-je prendre en compte lorsque je choisis entre les deux options?

En outre, comment cette option se compare-t-elle?

 void make_something(something_t *object) 

Quand something_t est petit (lisez: copier est à peu près aussi bon marché que copier un pointeur) et que vous voulez qu’il soit affecté par stack par défaut:

 something_t make_something(void); something_t stack_thing = make_something(); something_t *heap_thing = malloc(sizeof *heap_thing); *heap_thing = make_something(); 

Lorsque something_t est grand ou si vous voulez qu’il soit alloué en tas:

 something_t *make_something(void); something_t *heap_thing = make_something(); 

Indépendamment de la taille de something_t , et si vous ne vous souciez pas de savoir où il est alloué:

 void make_something(something_t *); something_t stack_thing; make_something(&stack_thing); something_t *heap_thing = malloc(sizeof *heap_thing); make_something(heap_thing); 

Cela concerne presque toujours la stabilité d’ABI. Stabilité binary entre les versions de la bibliothèque. Dans les cas où ce n’est pas le cas, il s’agit parfois d’avoir des structures de taille dynamic. Il s’agit rarement de struct ou de performances extrêmement importantes.


Il est extrêmement rare que l’allocation d’une struct sur le tas et son retour soit presque aussi rapide que son retour par valeur. La struct devrait être énorme.

En réalité, la vitesse n’est pas la raison derrière la technique 2, le retour par pointeur, plutôt que le retour par valeur.

La technique 2 existe pour la stabilité ABI. Si vous avez une struct et que votre prochaine version de la bibliothèque ajoute 20 champs supplémentaires, les consommateurs de votre version précédente de la bibliothèque sont compatibles avec les binarys s’ils reçoivent des pointeurs pré-construits. Les données supplémentaires au-delà de la struct qu’ils connaissent sont quelque chose qu’ils n’ont pas besoin de savoir.

Si vous le retournez sur la stack, l’appelant lui alloue la mémoire et il doit être d’accord avec vous sur sa taille. Si votre bibliothèque est mise à jour depuis la dernière reconstruction, vous allez éliminer la stack.

La technique 2 vous permet également de masquer des données supplémentaires à la fois avant et après le pointeur que vous retournez (les versions qui ajoutent des données à la fin de la structure). Vous pouvez terminer la structure avec un tableau de taille variable ou append le pointeur à des données supplémentaires, ou les deux.

Si vous voulez des struct allouées à la stack dans un ABI stable, presque toutes les fonctions qui parlent à la struct doivent recevoir des informations de version.

Alors

 something_t make_something(unsigned library_version) { ... } 

où bibliothèque_version est utilisée par la bibliothèque pour déterminer la version de something_t qu’elle est censée renvoyer et la quantité de stack manipulée . Ce n’est pas possible en utilisant le standard C, mais

 void make_something(something_t* here) { ... } 

est. Dans ce cas, something_t peut avoir un champ de version comme premier élément (ou un champ de taille), et vous devrez le remplir avant d’appeler make_something .

Un autre code de bibliothèque qui prend un something_t interrogerait alors le champ de version pour déterminer la version de something_t chose_t avec laquelle il travaille.

En règle générale, vous ne devez jamais passer d’objects de struct par valeur. En pratique, il sera bon de le faire tant qu’elles sont plus petites ou égales à la taille maximale que votre processeur peut gérer dans une seule instruction. Mais stylistiquement, on l’évite même alors. Si vous ne transmettez jamais de structure par valeur, vous pourrez ultérieurement append des membres à la structure et cela n’affectera pas les performances.

Je pense que void make_something(something_t *object) est le moyen le plus courant d’utiliser des structures en C. Vous laissez l’atsortingbution à l’appelant. C’est efficace mais pas joli.

Cependant, les programmes C orientés object utilisent something_t *make_something() car ils sont construits avec le concept de type opaque , ce qui vous oblige à utiliser des pointeurs. Que le pointeur renvoyé pointe vers la mémoire dynamic ou autre chose dépend de l’implémentation. OO avec le type opaque est souvent l’un des moyens les plus élégants et les plus efficaces pour concevoir des programmes en C plus complexes, mais malheureusement, peu de programmeurs en savent.

Quelques avantages de la première approche:

  • Moins de code à écrire.
  • Plus idiomatique pour le cas d’utilisation du retour de plusieurs valeurs.
  • Fonctionne sur les systèmes sans allocation dynamic.
  • Probablement plus rapide pour les objects petits ou petits.
  • Aucune fuite de mémoire due à l’oubli de free .

Quelques inconvénients:

  • Si l’object est volumineux (disons un mégaoctet), cela peut entraîner un dépassement de capacité de la stack ou peut être lent si les compilateurs ne l’optimisent pas correctement.
  • Peut surprendre les personnes qui ont appris le C dans les années 1970 alors que ce n’était pas possible et ne se sont pas tenues au courant.
  • Ne fonctionne pas avec des objects contenant un pointeur sur une partie de lui-même.

Je suis un peu surpris

La différence est que l’exemple 1 crée une structure sur la stack, l’exemple 2 le crée sur le tas. Dans le code C ou C ++ qui est effectivement C, il est idiomatique et pratique de créer la plupart des objects sur le tas. En C ++ ce n’est pas le cas, la plupart du temps ils vont sur la stack. La raison est que si vous créez un object sur la stack, le destructeur est appelé automatiquement, si vous le créez sur le tas, il doit être appelé explicitement. Il est donc beaucoup plus facile de s’assurer qu’il n’ya pas de memory leaks et de gérer les exceptions. tout se passe sur la stack. De toute façon, dans C, le destructeur doit être appelé explicitement, et il n’y a pas de concept de fonction destrucsortingce spéciale (vous avez bien sûr des destructeurs, mais ce ne sont que des fonctions normales avec des noms comme destroy_myobject ()).

Maintenant, l’exception en C ++ concerne les objects conteneur de bas niveau, tels que les vecteurs, les arbres, les cartes de hachage, etc. Celles-ci retiennent les membres du tas et ont des destructeurs. Maintenant, la plupart des objects contenant beaucoup de mémoire sont constitués de quelques membres de données immédiats donnant des tailles, des identifiants, des balises, etc., puis le rest des informations dans des structures STL. La plupart des données se trouvent donc sur le tas, même en C ++.

Et moderne C ++ est conçu pour que ce modèle

 class big { std::vector observations; // thousands of observations int station_x; // a bit of data associated with them int station_y; std::ssortingng station_name; } big resortingeveobservations(int a, int b, int c) { big answer; // lots of code to fill in the structure here return answer; } void high_level() { big myobservations = resortingveobservations(1, 2, 3); } 

Comstackr à un code assez efficace. Le grand membre d’observation ne générera pas de copies inutiles.

Contrairement à d’autres langages (comme Python), C n’a pas le concept de tuple . Par exemple, ce qui suit est légal en Python:

 def foo(): return 1,2 x,y = foo() print x, y 

La fonction foo renvoie deux valeurs sous la forme d’un tuple, atsortingbuées à x et y .

Comme C n’a pas le concept de tuple, il n’est pas pratique de renvoyer plusieurs valeurs d’une fonction. Une solution consiste à définir une structure pour contenir les valeurs, puis à renvoyer la structure, comme ceci:

 typedef struct { int x, y; } stPoint; stPoint foo( void ) { stPoint point = { 1, 2 }; return point; } int main( void ) { stPoint point = foo(); printf( "%d %d\n", point.x, point.y ); } 

Ce n’est qu’un exemple où une fonction peut renvoyer une structure.