Quel est l'avantage de renvoyer un pointeur sur une structure plutôt que de renvoyer toute la structure dans l' return
énoncé de la fonction?
Je parle de fonctions telles que fopen
et d'autres fonctions de bas niveau, mais il existe probablement des fonctions de niveau supérieur qui renvoient également des pointeurs vers des structures.
Je pense qu'il s'agit davantage d'un choix de conception que d'une simple question de programmation et je suis curieux d'en savoir plus sur les avantages et les inconvénients des deux méthodes.
L'une des raisons pour lesquelles je pensais que ce serait un avantage de renvoyer un pointeur sur une structure est de pouvoir dire plus facilement si la fonction a échoué en renvoyant un NULL
pointeur.
Rendre une structure complèteNULL
serait plus difficile ou moins efficace, je suppose. Est-ce une raison valable?
la source
gets()
fonction totalement mal conçue soit supprimée. Certains programmeurs ont encore une aversion pour la copie des structures, les vieilles habitudes ont la vie dure.FILE*
est effectivement une poignée opaque. Le code utilisateur ne doit pas se soucier de sa structure interne.&
et accéder à un membre avec.
."Réponses:
Il existe plusieurs raisons pratiques pour lesquelles des fonctions telles que
fopen
renvoyer des pointeurs vers au lieu d'instances destruct
types:struct
type à l'utilisateur;Dans le cas des types comme
FILE *
, il est parce que vous ne voulez pas exposer les détails de la représentation du genre à l'utilisateur - unFILE *
objet sert une poignée opaque, et vous passez juste que la poignée à diverses routines d' E / S (et toutFILE
est souvent mis en œuvre en tant questruct
type, il n'a pas a être).Vous pouvez donc exposer un type incomplet
struct
dans un en-tête quelque part:Bien que vous ne puissiez pas déclarer une instance d'un type incomplet, vous pouvez déclarer un pointeur sur elle. Je peux donc créer un
FILE *
et l'assigner au traversfopen
,freopen
etc., mais je ne peux pas manipuler directement l'objet sur lequel il pointe.Il est également probable que la
fopen
fonction alloue unFILE
objet de manière dynamique,malloc
similaire ou similaire. Dans ce cas, il est logique de renvoyer un pointeur.Enfin, il est possible que vous stockiez un type d'état dans un
struct
objet et que vous deviez le rendre disponible à différents endroits. Si vous renvoyiez des instances dustruct
type, ces instances seraient des objets séparés en mémoire les unes des autres et finiraient par ne plus être synchronisées. En renvoyant un pointeur sur un seul objet, tout le monde se réfère au même objet.la source
Il y a deux façons de "retourner une structure". Vous pouvez renvoyer une copie des données ou une référence (pointeur). Il est généralement préférable de retourner (et de passer en général) un pointeur, pour plusieurs raisons.
Tout d'abord, la copie d'une structure prend beaucoup plus de temps processeur que la copie d'un pointeur. Si votre code est fréquemment utilisé, cela peut entraîner une différence de performances notable.
Deuxièmement, quel que soit le nombre de fois où vous copiez un pointeur, il pointe toujours sur 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 apportez une modification, la modification apparaît uniquement sur cette copie . Tout code contenant une copie différente ne verra pas le changement. Parfois, très rarement, c'est ce que vous voulez, mais la plupart du temps, ce n'est pas le cas et cela peut causer des bugs si vous vous trompez.
la source
En plus d’autres réponses, il peut parfois être utile de renvoyer une petite
struct
valeur. Par exemple, on peut renvoyer une paire de données et un code d’erreur (ou de succès) qui y est associé.Pour prendre un exemple,
fopen
renvoie une seule donnée (la donnée ouverteFILE*
) et, en cas d'erreur, donne le code d'erreur via laerrno
variable pseudo-globale. Mais il serait peut-être préférable de renvoyer unstruct
des deux membres: leFILE*
descripteur et le code d'erreur (qui serait défini si le descripteur de fichier estNULL
). Pour des raisons historiques, ce n'est pas le cas (et les erreurs sont signalées via leerrno
global, qui est aujourd'hui une macro).Notez que le langage Go a une bonne notation pour renvoyer deux (ou quelques) valeurs.
Notez également que, sur Linux / x86-64, les conventions ABI et d’appel (voir la page x86-psABI ) spécifient qu’un membre
struct
scalaire sur deux (par exemple, un pointeur et un entier, ou deux pointeurs, ou deux entiers) est renvoyé dans deux registres. (et cela est 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 aux threads et plus efficace.la source
rdx:rax
. Sostruct foo { int a,b; };
est renvoyé dansrax
(par exemple avec shift / ou) et doit être déballé avec shift / mov. Voici un exemple sur Godbolt . Mais x86 peut utiliser les 32 bits les plus bas d’un registre 64 bits pour les opérations 32 bits sans se soucier des bits les plus élevés. C’est donc toujours dommage, mais bien pire que d’utiliser 2 registres dans la plupart du temps pour des structures à 2 membres.std::optional<int>
renvoie le booléen dans la moitié supérieure derax
, vous avez donc besoin d'une constante de masque 64 bits pour le testertest
. Ou vous pourriez utiliserbt
. Mais ça craint pour l'appelant et l'appelé comparé à utiliserdl
, ce que les compilateurs devraient faire pour les fonctions "privées". Également lié: libstdc ++std::optional<T>
n'est pas copiable de manière triviale même lorsque T l'est; il est donc toujours renvoyé via un pointeur masqué: stackoverflow.com/questions/46544019/… . (la libc ++ est trivialement copiable)struct { int a; _Bool b; };
C, si l’appelant voulait tester le booléen, car les structures C ++ trivialement copiables utilisent le même ABI que C.div_t div()
Vous êtes sur la bonne voie
Les deux raisons que vous avez mentionnées sont valables:
Si vous avez une texture (par exemple) quelque part en mémoire et que vous souhaitez référencer cette texture à plusieurs endroits de votre programme; il ne serait pas sage de faire une copie chaque fois que vous vouliez vous y référer. Au lieu de cela, si vous passez simplement un pointeur pour référencer la texture, votre programme s'exécutera beaucoup plus rapidement.
La principale raison est l'allocation dynamique de la mémoire. Souvent, lorsqu'un programme est compilé, vous ne savez pas exactement de combien de mémoire vous avez besoin pour certaines structures de données. Lorsque cela se produit, la quantité de mémoire à utiliser sera déterminée lors de l'exécution. Vous pouvez demander de la mémoire en utilisant 'malloc' puis la libérer lorsque vous avez fini d'utiliser 'free'.
Un bon exemple de ceci est la lecture d'un fichier spécifié par l'utilisateur. Dans ce cas, vous n'avez aucune idée de la taille du fichier lors de la compilation du programme. Vous ne pouvez déterminer que la quantité de mémoire dont vous avez besoin lorsque le programme est en cours d'exécution.
Malloc et free renvoient des pointeurs vers des emplacements en mémoire. Ainsi, les fonctions qui utilisent l'allocation dynamique de la mémoire renverront des pointeurs vers l'endroit où ils ont créé leurs structures en mémoire.
De plus, dans les commentaires, je vois qu'il y a une question de savoir si vous pouvez retourner une structure à partir d'une fonction. Vous pouvez en effet faire cela. Ce qui suit devrait fonctionner:
la source
struct incomplete* foo(void)
. De cette façon, je peux déclarer des fonctions dans un en-tête, mais seulement définir les structures dans un fichier C, permettant ainsi l'encapsulation.Quelque chose comme un
FILE*
n'est pas vraiment un pointeur sur une structure en ce qui concerne le code client, mais plutôt une forme d'identificateur opaque associé à une autre entité comme un fichier. Lorsqu'un programme appellefopen
, il ne se soucie généralement pas du contenu de la structure renvoyée. Tout ce qui le concerne, c'est que d'autres fonctions, comme celle-fread
ci, feront tout ce dont elles ont besoin.Si une bibliothèque standard conserve dans une
FILE*
information concernant, par exemple, la position de lecture actuelle dans ce fichier, un appel àfread
doit avoir la possibilité de mettre à jour cette information. Avoirfread
reçu un pointeur sur leFILE
rend aussi facile. Sifread
au lieu de cela reçu unFILE
, il n'aurait aucun moyen de mettre à jour l'FILE
objet détenu par l'appelant.la source
Masquage de l'information
Le plus commun est la dissimulation d'informations . C n’a pas, par exemple, la capacité de créer des champs d’un domaine
struct
privé, et encore moins de fournir des méthodes pour y accéder.Donc, si vous voulez empêcher avec force les développeurs de voir et d'altérer le contenu d'une pointe, par exemple
FILE
, le seul moyen consiste à les empêcher d'être exposés à sa définition en traitant le pointeur comme un opaque dont la taille et le définition sont inconnus du monde extérieur. La définition deFILE
ne sera alors visible que pour ceux qui implémentent les opérations qui nécessitent sa définition, commefopen
, tandis que seule la déclaration de structure sera visible pour l'en-tête public.Compatibilité binaire
Masquer 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. Il permet aux utilisateurs de la bibliothèque de modifier les champs de la structure opaque sans rompre la compatibilité binaire avec ceux qui utilisent la bibliothèque, car la nature de leur code a uniquement besoin de savoir ce qu’ils peuvent faire avec la structure, et non de la taille ou des champs. il a.
Par exemple, je peux réellement exécuter des programmes anciens construits sous Windows 95 (pas toujours parfaitement, mais étonnamment, beaucoup fonctionnent encore). Il est fort probable qu'une partie du code de ces anciens fichiers binaires utilisait des pointeurs opaques vers des structures dont la taille et le contenu avaient changé depuis 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, les éléments auxquels le client n'est pas exposé sont généralement autorisés à changer sans que la compatibilité en amont ne soit supprimée.
Efficacité
Il est généralement moins efficace de supposer que le type peut correspondre à la pile et être alloué sur la pile, sauf si un allocateur de mémoire beaucoup moins généralisé est utilisé en coulisse
malloc
, comme une mémoire de mise en pool allouée de taille fixe plutôt que de taille variable. Dans ce cas, c’est très probablement un compromis en matière de sécurité que de permettre aux développeurs de bibliothèques de conserver des invariants (garanties conceptuelles) liés àFILE
.Ce n'est pas une raison valable, du moins du point de vue des performances, de
fopen
renvoyer un pointeur, car la seule raison de son retourNULL
est l'échec de l'ouverture d'un fichier. Ce serait optimiser un scénario exceptionnel en contrepartie du ralentissement de tous les chemins d’exécution courants. Il peut y avoir une raison valable de productivité dans certains cas pour rendre les conceptions plus simples et leur permettre de renvoyer des pointeurs permettantNULL
de renvoyer des post-conditions.Pour les opérations sur les fichiers, le temps système est relativement trivial par rapport aux opérations sur les fichiers eux-mêmes, et le manuel
fclose
ne doit de toute façon pas être évité. Donc, ce n'est pas comme si nous pouvions éviter au client la tâche de libérer (fermer) la ressource en exposant la définition deFILE
et en la renvoyant par valeurfopen
ou en espérant une amélioration significative des performances compte tenu du coût relatif des opérations de fichier elles-mêmes afin d'éviter une allocation en tas .Hotspots et correctifs
Dans d’autres cas, j’ai décrit beaucoup de code C inutile dans des bases de code héritées avec des points chauds
malloc
et des ratés inutiles dans le cache en raison de l’utilisation trop fréquente de cette pratique avec des pointeurs opaques et de l’affectation inutile de trop nombreuses ressources inutilement, grandes boucles.Une pratique alternative que j'utilise à la place consiste à exposer les définitions de structure, même si le client n'est pas censé les altérer, en utilisant un standard de convention de dénomination communiquant que personne d'autre ne devrait toucher aux champs:
S'il y a des problèmes de compatibilité binaire à l'avenir, j'ai trouvé qu'il était suffisant de réserver de manière superflue de l'espace supplémentaire à des fins futures, comme ceci:
Cet espace réservé est un peu inutile, mais peut vous sauver la vie si nous découvrons à l'avenir que nous devons ajouter des données supplémentaires
Foo
sans casser les fichiers binaires qui utilisent notre bibliothèque.À mon avis , le masquage d'informations et la compatibilité binaire sont généralement la seule raison convenable de n'autoriser que l'allocation de tas de structures en plus des structures de longueur variable (ce qui l'exigerait toujours, ou du moins serait un peu gênant si le client devait attribuer mémoire sur la pile en mode VLA pour allouer le VLS). Même les grandes structures sont souvent moins chères à retourner en valeur si cela signifie que le logiciel travaille beaucoup plus avec la mémoire chaude de la pile. Et même si cela ne coûtait pas moins cher de revenir en valeur sur la création, on pourrait simplement faire ceci:
... pour initialiser à
Foo
partir de la pile sans possibilité de copie superflue. Ou le client a même la liberté d'allouerFoo
sur le tas s'il le souhaite pour une raison quelconque.la source