Pourquoi de nombreuses fonctions qui renvoient des structures en C renvoient en fait des pointeurs vers des structures?

Quel est l’avantage de renvoyer un pointeur vers une structure par opposition à renvoyer la structure entière dans le return déclaration de la fonction?

Je parle de fonctions comme fopen et dautres fonctions de bas niveau, mais il existe probablement des fonctions de plus haut niveau qui renvoient également des pointeurs vers des structures.

Je pense quil sagit plus dun choix de conception que dune simple question de programmation et je suis curieux den savoir plus sur les avantages et les inconvénients des deux méthodes.

Lune des raisons pour lesquelles je pensais que ce serait un avantage de renvoyer un pointeur vers une structure est de pouvoir dire plus facilement si la fonction a échoué en renvoyant le pointeur NULL.

Renvoyer une structure complète qui est NULL serait plus difficile je suppose ou moins efficace. Est-ce une raison valable?

Commentaires

  • @ JohnR.Strohm Je lai essayé et ça marche réellement. Une fonction peut renvoyer une structure …. Alors, quelle est la raison pour laquelle ce nest pas fait?
  • La pré-standardisation C ne permettait pas de copier ou de passer des structures par valeur. La bibliothèque standard C a de nombreux restes de cette époque qui ne seraient pas écrits de cette façon aujourdhui, par exemple il a fallu attendre C11 pour que la fonction gets() totalement mal conçue soit supprimée. Certains programmeurs ont encore une aversion pour la copie de structs, les vieilles habitudes sont mortes.
  • FILE* est en fait un handle opaque. Le code utilisateur ne doit pas se soucier de sa structure interne.
  • Le retour par référence nest une valeur par défaut raisonnable que lorsque vous avez un garbage collection.
  • @ JohnR.Strohm Le  » très senior  » dans votre profil semble remonter avant 1989 😉 – quand ANSI C a permis ce que K & RC didn ‘ t: Copie des structures dans les affectations, passage des paramètres et retour des valeurs Le livre original de K & R ‘ a en effet déclaré explicitement (je ‘ m paraphrasant):  » vous pouvez faire exactement deux choses avec une structure, prendre son adresse avec & et accéder à un membre avec ..  »

Réponse

Là Il existe plusieurs raisons pratiques pour lesquelles des fonctions telles que fopen renvoient des pointeurs vers au lieu dinstances de types struct:

  1. Vous souhaitez masquer la représentation du type struct à lutilisateur;
  2. Vous « allouez un objet dynamiquement;
  3. Vous » êtes faisant référence à une seule instance dun objet via plusieurs références;

Dans le cas de types comme FILE *, cest parce que vous ne le faites pas voulez exposer les détails de la représentation du type à lutilisateur – un FILE * obje ct sert de handle opaque, et vous passez simplement ce handle à diverses routines dE / S (et alors que FILE est souvent implémenté en tant que struct type, il ne doit pas être ).

Ainsi, vous pouvez exposer un type incomplet struct dans un en-tête quelque part:

typedef struct __some_internal_stream_implementation FILE; 

Bien que vous ne puissiez pas déclarer une instance de type incomplet, vous pouvez déclarer un pointeur vers elle. Je peux donc créer un FILE * et lattribuer via fopen, freopen, etc. , mais je ne peux pas manipuler directement lobjet vers lequel il pointe.

Il est également probable que la fonction fopen alloue un FILE objet dynamiquement, en utilisant malloc ou similaire. Dans ce cas, il est logique de renvoyer un pointeur.

Enfin, il est possible que vous stockiez une sorte détat dans un objet struct, et que vous deviez rendre cet état disponible à plusieurs endroits différents. Si vous renvoyiez des instances de type struct, ces instances seraient des objets séparés en mémoire les uns des autres et finiraient par se désynchroniser. En renvoyant un pointeur vers un seul objet, tout le monde fait référence au même objet.

Commentaires

  • Un avantage particulier à utiliser le pointeur comme un le type opaque est que la structure elle-même peut changer entre les versions de la bibliothèque et que vous navez ‘ pas besoin de recompiler les appelants.
  • @Barmar: En effet, la stabilité ABI est lénorme argument de vente de C, et il ne serait pas aussi stable sans pointeurs opaques.

Answer

Il y a deux manières de » renvoyer une structure « . Vous pouvez renvoyer une copie des données ou vous pouvez renvoyer une référence (pointeur) à celle-ci.Il est généralement préférable de renvoyer (et de faire circuler en général) un pointeur, pour plusieurs raisons.

Premièrement, copier une structure prend beaucoup plus de temps CPU que copier un pointeur. Si cest quelque chose votre code le fait fréquemment, cela peut entraîner une différence de performance notable.

Deuxièmement, peu importe combien de fois vous copiez un pointeur, il pointe toujours vers la même structure en mémoire. Toutes les modifications apportées seront reflétées sur la même structure. Mais si vous copiez la structure elle-même, puis effectuez une modification, la modification napparaîtra que sur cette copie . Tout code contenant une copie différente ne verra pas le changement. Parfois, très rarement, cest ce que vous voulez, mais la plupart du temps ce nest pas le cas, et cela peut provoquer des bogues si vous vous trompez.

Commentaires

  • Linconvénient de renvoyer par pointeur: maintenant vous ‘ avez à suivre la propriété de cet objet et possible le libérer. En outre, lindirection du pointeur peut être plus coûteuse quune copie rapide. Il y a beaucoup de variables ici, donc lutilisation de pointeurs nest pas universellement meilleure.
  • De plus, les pointeurs de nos jours sont 64 bits sur la plupart des plates-formes de bureau et de serveur. Jai ‘ vu plus de quelques structures dans ma carrière qui tiendraient en 64 bits. Ainsi, vous pouvez ‘ t toujours dire que copier un pointeur coûte moins cher que copier une structure.
  • Cest surtout une bonne réponse , mais je ne suis pas daccord sur la partie parfois, très rarement, cest ce que vous voulez, mais la plupart du temps, ‘ nest pas – bien au contraire. Le renvoi dun pointeur permet plusieurs types deffets secondaires indésirables et plusieurs types de moyens désagréables de se tromper sur la propriété dun pointeur. Dans les cas où le temps CPU nest pas si important, je préfère la variante de copie, si cest une option, elle est beaucoup moins sujette aux erreurs.
  • Il convient de noter que cest vraiment sapplique uniquement aux API externes. Pour les fonctions internes, chaque compilateur, même marginalement compétent, des dernières décennies réécrira une fonction qui renvoie une grande structure pour prendre un pointeur comme argument supplémentaire et y construire lobjet directement. Les arguments immuable vs mutable ont été assez souvent utilisés, mais je pense que nous pouvons tous convenir que laffirmation selon laquelle les structures de données immuables ne sont presque jamais ce que vous voulez nest pas vraie.
  • Vous pouvez également mentionner les pare-feu de compilation en tant que pro pour les pointeurs. Dans les grands programmes avec des en-têtes largement partagés, les types incomplets avec des fonctions évitent davoir à recompiler chaque fois quun détail dimplémentation change. Le meilleur comportement de compilation est en fait un effet secondaire de lencapsulation qui est obtenu lorsque linterface et limplémentation sont séparées. Le renvoi (et le passage, lattribution) par valeur nécessitent les informations dimplémentation.

Réponse

En plus dautres réponses , parfois renvoyer un petit struct par valeur vaut la peine. Par exemple, on pourrait renvoyer une paire dune donnée, et un code derreur (ou de réussite) qui lui est lié.

Pour prendre un exemple, fopen renvoie juste une donnée (le FILE* ouvert) et en cas derreur, donne le code derreur via le errno variable pseudo-globale. Mais il serait peut-être préférable de renvoyer un struct de deux membres: le handle FILE* et le code derreur (qui serait défini si le descripteur de fichier est NULL). Pour des raisons historiques, ce nest pas le cas (et les erreurs sont signalées via le errno global, qui est aujourdhui une macro).

Notez que le Go language a une bonne notation pour renvoyer deux (ou quelques) valeurs.

Notez également que sous Linux / x86-64, le ABI et conventions dappel (voir la page x86-psABI ) spécifie quun struct de deux membres scalaires (par exemple un pointeur et un entier, ou deux pointeurs, ou deux entiers) est retourné à travers deux registres (et cest très efficace et ne passe pas par la mémoire).

Ainsi, dans le nouveau code C, renvoyer un petit C struct peut être plus lisible, plus convivial pour les threads et plus efficace.

Commentaires

  • En fait, les petites structures sont compactées dans rdx:rax. Ainsi, struct foo { int a,b; }; est retourné compressé dans rax (par exemple avec shift / ou), et doit être décompressé avec shift / mov. Voici ‘ un exemple sur Godbolt . Mais x86 peut utiliser les 32 bits bas dun registre 64 bits pour des opérations 32 bits sans se soucier des bits hauts, donc ‘ est toujours trop mauvais, mais certainement pire que dutiliser 2 enregistre la plupart du temps pour les structures à 2 membres.
  • Connexes: bugs.llvm.org/show_bug.cgi? id = 34840 std::optional<int> renvoie le booléen dans la moitié supérieure de rax, vous avez donc besoin dun masque 64 bits constante pour le tester avec test. Ou vous pouvez utiliser bt. Mais cest nul pour lappelant et lappelé par rapport à lutilisation de dl, ce que les compilateurs devraient faire pour  » private  » fonctions. Également lié: libstdc ++ ‘ s std::optional<T> isn ‘ t trivialement copiable même lorsque T est , il renvoie donc toujours via un pointeur masqué: stackoverflow.com/questions/46544019/… . (libc ++ ‘ s est trivialement copiable)
  • @PeterCordes: vos éléments associés sont C ++, pas C
  • Oups, cest vrai. Eh bien, la même chose sappliquerait exactement à struct { int a; _Bool b; }; en C, si lappelant voulait tester le booléen, car les structures C ++ facilement copiables utilisent le même ABI que C.
  • Exemple classique div_t div()

Réponse

Vous êtes sur la bonne voie

Les deux raisons que vous avez mentionnées sont valables:

Lune des raisons pour lesquelles je pensé que ce serait un avantage de renvoyer un pointeur vers une structure est de pouvoir dire plus facilement si la fonction a échoué en renvoyant un pointeur NULL.

Renvoyer une structure FULL qui est NULL serait plus difficile je suppose ou moins efficace. Est-ce une raison valable?

Si vous avez une texture (par exemple) quelque part en mémoire, et que vous souhaitez référencer cette texture à plusieurs endroits dans votre programme; il ne serait pas judicieux de faire une copie à chaque fois que vous voudriez le référencer. Au lieu de cela, si vous passez simplement un pointeur pour référencer la texture, votre programme fonctionnera beaucoup plus vite.

La principale raison cependant est lallocation de mémoire dynamique. Souvent, lorsquun programme est compilé, vous ne savez pas exactement de quelle quantité de mémoire vous avez besoin pour certaines structures de données. Lorsque cela se produit, la quantité de mémoire que vous devez utiliser sera déterminée au moment de lexécution. Vous pouvez demandez de la mémoire en utilisant malloc, puis libérez-la lorsque vous avez fini dutiliser free.

Un bon exemple de ceci est la lecture dun fichier spécifié par lutilisateur. Dans ce cas, vous navez pas idée de la taille du fichier lorsque vous compilez le programme. Vous ne pouvez déterminer la quantité de mémoire dont vous avez besoin que lorsque le programme est en cours d’exécution.

Malloc et free renvoient des pointeurs vers des emplacements en mémoire. qui utilisent lallocation de mémoire dynamique renverront des pointeurs vers lendroit où ils ont créé leurs structures en mémoire.

De plus, dans les commentaires, je vois quil y a une question de savoir si vous pouvez renvoyer une structure à partir dune fonction. Vous pouvez en effet faire cela. Ce qui suit devrait fonctionner:

struct s1 { int integer; }; struct s1 f(struct s1 input){ struct s1 returnValue = xinput return returnValue; } int main(void){ struct s1 a = { 42 }; struct s1 b= f(a); return 0; } 

Commentaires

  • Comment est-il possible de ne pas savoir combien de mémoire une certaine variable aura besoin si vous avez déjà défini le type struct?
  • @JenniferAnderson C a un concept de types incomplets: un nom de type peut être déclaré mais pas encore défini, donc il ‘ nest pas disponible. Je ne peux pas déclarer de variables de ce type, mais je peux déclarer des pointeurs vers ce type, par exemple struct incomplete* foo(void). De cette façon, je peux déclarer des fonctions dans un en-tête, mais définir uniquement les structures dans un fichier C, permettant ainsi lencapsulation.
  • @amon Donc, voici comment déclarer les en-têtes de fonction (prototypes / signatures) avant de déclarer comment ils le travail est-il réellement fait en C? Et il est possible de faire la même chose pour les structures et les unions en C
  • @JenniferAnderson vous déclarez des fonctions prototypes (fonctions sans corps) dans des fichiers den-tête et pouvez ensuite appeler ces fonctions dans un autre code, sans connaître le corps des fonctions, car le compilateur a juste besoin de savoir comment organiser les arguments et comment accepter la valeur de retour. Au moment où vous liez le programme, vous devez en fait connaître la définition de la fonction (cest-à-dire avec un corps), mais vous ne devez la traiter quune seule fois. Si vous utilisez un type non simple, il doit également connaître la structure de ce type ‘, mais les pointeurs sont souvent de la même taille et cela n’a pas ‘ t importe pour lutilisation dun prototype ‘.

Réponse

Quelque chose comme un FILE* nest pas vraiment un pointeur vers une structure en ce qui concerne le code client, mais est plutôt une forme didentifiant opaque associé à un autre entité comme un fichier. Lorsquun programme appelle fopen, il ne se souciera généralement pas du contenu de la structure retournée – tout ce dont il se souciera est que dautres fonctions comme fread feront tout ce dont elles ont besoin pour en faire.

Si une bibliothèque standard contient des informations FILE* sur par exemple la position de lecture actuelle dans ce fichier, un appel à fread devrait être en mesure de mettre à jour ces informations. Le fait que fread reçoive un pointeur vers FILE rend cela facile. Si fread recevait à la place un FILE, il naurait aucun moyen de mettre à jour lobjet FILE tenu par lappelant.

Réponse

Masquage dinformations

Quel est lavantage de renvoyer un pointeur vers une structure plutôt que de renvoyer la structure entière dans linstruction de retour de la fonction?

La plus courante est le masquage dinformations . C na pas, disons, la possibilité de rendre les champs dun struct privés, et encore moins de fournir des méthodes pour y accéder.

Donc, si vous voulez forcer empêcher les développeurs de voir et de falsifier le contenu dune pointee, comme FILE, alors le seul et unique moyen est de les empêcher dêtre exposés à sa définition en traitant le pointeur comme opaque dont la taille et la définition de pointe sont inconnues du monde extérieur. La définition de FILE ne sera alors visible que par ceux qui implémentent les opérations qui nécessitent sa définition, comme fopen, alors que seule la déclaration de structure sera visible par len-tête public.

Compatibilité binaire

Le masquage de la définition de la structure peut également aider à fournir une marge de manœuvre pour préserver la compatibilité binaire dans les API dylib. Cela permet aux implémenteurs de la bibliothèque de modifier les champs de la structure opaque ure sans briser la compatibilité binaire avec ceux qui utilisent la bibliothèque, car la nature de leur code a seulement besoin de savoir ce quils peuvent faire avec la structure, pas sa taille ou ses champs.

En tant que exemple, je peux en fait exécuter certains programmes anciens construits à lépoque de Windows 95 aujourdhui (pas toujours parfaitement, mais étonnamment beaucoup fonctionnent encore). Il est fort probable quune partie du code de ces anciens binaires utilisait des pointeurs opaques vers des structures dont la taille et le contenu ont changé depuis lère Windows 95. Pourtant, les programmes continuent de fonctionner dans les nouvelles versions de Windows car ils n’ont pas été exposés au contenu de ces structures. Lorsque vous travaillez sur une bibliothèque où la compatibilité binaire est importante, ce à quoi le client n’est pas exposé est généralement autorisé à changer sans rupture rétrocompatibilité.

Efficacité

Renvoyer une structure complète qui est NULL serait plus difficile je suppose ou moins efficace. Est-ce une raison valable?

Il est généralement moins efficace en supposant que le type peut pratiquement sadapter et être alloué sur la pile à moins quil ny en ait généralement beaucoup moins lallocateur de mémoire généralisé utilisé dans les coulisses que malloc, comme une mémoire de mise en pool dallocateur de taille fixe plutôt que de taille variable déjà allouée. Cest un compromis de sécurité dans ce cas, la plupart probablement, pour permettre aux développeurs de bibliothèques de maintenir des invariants (garanties conceptuelles) liés à FILE.

Ce nest pas une raison aussi valable, du moins du point de vue des performances pour que fopen renvoie un pointeur puisque la seule raison pour laquelle il « d return NULL est léchec de louverture dun fichier. Ce serait optimiser un scénario exceptionnel en échange dun ralentissement de tous les chemins dexécution de cas courants. Il peut y avoir une raison de productivité valable dans certains cas pour rendre les conceptions plus simples pour les rendre renvoyées par des pointeurs permettant de renvoyer NULL sur une condition postérieure.

Pour les opérations sur les fichiers, la surcharge est relativement assez insignifiante par rapport aux opérations sur les fichiers elles-mêmes, et le besoin manuel de fclose ne peut pas être évité de toute façon. Donc, ce nest pas comme si nous pouvions épargner au client les tracas de libérer (fermer) la ressource en exposant la définition de FILE et en la renvoyant par valeur dans fopen ou attendez-vous à une amélioration des performances compte tenu du coût relatif des opérations sur les fichiers elles-mêmes pour éviter une allocation de tas.

Hotspots et correctifs

Pour dautres cas cependant, jai profilé beaucoup de code C inutile dans des bases de code héritées avec des hotspots dans malloc et des échecs de cache obligatoires inutiles en raison de lutilisation trop fréquente de cette pratique avec des pointeurs opaques et de lallocation inutile de trop de choses sur le tas, parfois dans de grandes boucles.

Une autre pratique que jutilise à la place est dexposer les définitions de structure, même si le client nest pas censé les falsifier, en utilisant une convention de dénomination standard pour communiquer que personne dautre ne devrait toucher les champs:

struct Foo { /* priv_* indicates that you shouldn"t tamper with these fields! */ int priv_internal_field; int priv_other_one; }; struct Foo foo_create(void); void foo_destroy(struct Foo* foo); void foo_something(struct Foo* foo); 

Sil y a des problèmes de compatibilité binaire à lavenir, alors je lai trouvé assez bon pour réserver de manière superflue de lespace supplémentaire à des fins futures, comme ceci:

struct Foo { /* priv_* indicates that you shouldn"t tamper with these fields! */ int priv_internal_field; int priv_other_one; /* reserved for possible future uses (emergency backup plan). currently just set to null. */ void* priv_reserved; }; 

Cet espace réservé est un peu inutile mais peut nous sauver la vie si nous constatons à lavenir que nous devons ajouter des données supplémentaires à Foo sans casser les binaires qui utilisent notre bibliothèque.

À mon avis, le masquage dinformations et la compatibilité binaire sont généralement la seule bonne raison dautoriser uniquement lallocation de tas de structures en plus des structures de longueur variable (ce qui lexigerait toujours, ou du moins serait un peu difficile à utiliser sinon si le client devait allouer de la mémoire sur la pile dans un mode VLA d’allouer le VLS). Même les structures volumineuses sont souvent moins chères à renvoyer par valeur si cela signifie que le logiciel travaille beaucoup plus avec la mémoire chaude de la pile. Et même sils nétaient « pas moins chers à rendre par valeur à la création, on pourrait simplement faire ceci:

int foo_create(struct Foo* foo); ... /* In the client code: */ struct Foo foo; if (foo_create(&foo)) { foo_something(&foo); foo_destroy(&foo); } 

… pour initialiser Foo depuis la pile sans possibilité de copie superflue. Ou le client a même la liberté dallouer Foo sur le tas sil le souhaite pour une raison quelconque.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *