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
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
:
- Vous souhaitez masquer la représentation du type
struct
à lutilisateur; - Vous « allouez un objet dynamiquement;
- 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é dansrax
(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 derax
, vous avez donc besoin dun masque 64 bits constante pour le tester avectest
. Ou vous pouvez utiliserbt
. Mais cest nul pour lappelant et lappelé par rapport à lutilisation dedl
, ce que les compilateurs devraient faire pour » private » fonctions. Également lié: libstdc ++ ‘ sstd::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.
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.&
et accéder à un membre avec.
. »