Pourquoi de nombreuses fonctions qui renvoient des structures en C renvoient-elles en fait des pointeurs sur des structures?

49

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 fopenet 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 NULLpointeur.

Rendre une structure complèteNULL serait plus difficile ou moins efficace, je suppose. Est-ce une raison valable?

yoyo_fun
la source
10
@ JohnR.Strohm Je l'ai essayé et cela fonctionne réellement. Une fonction peut retourner une struct .... Alors, quelle est la raison pour laquelle ne pas fait?
yoyo_fun
28
La pré-normalisation C n’autorisait pas la copie des structures ni leur transmission par valeur. La bibliothèque standard C contient de nombreux éléments de cette époque qui ne seraient plus écrits de la sorte, par exemple, il a fallu attendre C11 pour que la 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.
amon
26
FILE*est effectivement une poignée opaque. Le code utilisateur ne doit pas se soucier de sa structure interne.
CodesInChaos
3
Renvoyer par référence n'est qu'un paramètre raisonnable par défaut lorsque vous disposez d'un ramasse-miettes.
Idan Arye
7
@ JohnR.Strohm Le "très senior" de votre profil semble remonter avant 1989 ;-) - lorsque ANSI C permettait ce que K & R C ne permettait pas: copier des structures dans des affectations, transmettre des valeurs et renvoyer des valeurs. Le livre original de K & R a en effet déclaré explicitement (je vais paraphraser): "vous pouvez faire exactement deux choses avec une structure, prendre son adresse avec & et accéder à un membre avec .."
Peter - Réintégrer Monica

Réponses:

61

Il existe plusieurs raisons pratiques pour lesquelles des fonctions telles que fopenrenvoyer des pointeurs vers au lieu d'instances de structtypes:

  1. Vous voulez masquer la représentation du structtype à l'utilisateur;
  2. Vous allouez un objet de manière dynamique.
  3. Vous faites référence à une seule instance d'un objet via plusieurs références;

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 - un FILE *objet sert une poignée opaque, et vous passez juste que la poignée à diverses routines d' E / S (et tout FILEest souvent mis en œuvre en tant que structtype, il n'a pas a être).

Vous pouvez donc 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 d'un type incomplet, vous pouvez déclarer un pointeur sur elle. Je peux donc créer un FILE *et l'assigner au travers fopen, freopenetc., mais je ne peux pas manipuler directement l'objet sur lequel il pointe.

Il est également probable que la fopenfonction alloue un FILEobjet de manière dynamique, mallocsimilaire 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 structobjet et que vous deviez le rendre disponible à différents endroits. Si vous renvoyiez des instances du structtype, 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.

John Bode
la source
31
L’utilisation du pointeur en tant que type opaque présente un avantage particulier: la structure elle-même peut changer d’une version à l’autre de la bibliothèque et il n’est pas nécessaire de recompiler les appelants.
Barmar
6
@Barmar: En effet, ABI Stability est l’ énorme argument de vente de C, et il ne serait pas aussi stable sans pointeurs opaques.
Matthieu M.
37

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.

Maçon Wheeler
la source
54
L'inconvénient de revenir par pointeur: vous devez maintenant suivre la propriété de cet objet et le libérer. En outre, l'indirection de pointeur peut être plus coûteuse qu'une copie rapide. Il y a beaucoup de variables ici, donc utiliser des pointeurs n'est pas toujours meilleur.
amon
17
De plus, les pointeurs actuels sont en 64 bits sur la plupart des plates-formes de bureau et de serveur. J'ai vu plus que quelques structures dans ma carrière qui cadreraient en 64 bits. Donc, vous ne pouvez pas toujours dire que la copie d'un pointeur coûte moins cher que la copie d'une structure.
Salomon Slow
37
C'est généralement une bonne réponse, mais je ne suis pas d'accord parfois, très rarement, c'est ce que vous voulez, mais la plupart du temps, ce n'est pas - au contraire. Le renvoi d'un pointeur permet plusieurs types d'effets secondaires indésirables et plusieurs types de méthodes désagréables pour obtenir la propriété d'un pointeur. Dans les cas où le temps de calcul n’est pas si important, je préfère la variante de copie; si c’est une option, elle est beaucoup moins sujette aux erreurs.
Doc Brown
6
Il convient de noter que cela ne s'applique réellement qu'aux API externes. Pour les fonctions internes, tous les compilateurs même les plus marginaux des dernières décennies réécriront une fonction qui renvoie une structure de grande taille pour prendre un pointeur en tant qu'argument supplémentaire et y construire l'objet directement. Les arguments immuable vs mutable ont été suffisamment souvent invoqués, mais je pense que nous pouvons tous convenir que l'affirmation selon laquelle les structures de données immuables ne correspondent presque jamais à ce que vous voulez n'est pas vraie.
Voo
6
Vous pouvez également mentionner les murs coupe-feu en tant que professionnels pour les pointeurs. Dans les gros programmes avec des en-têtes largement partagés, les types incomplets avec des fonctions évitent la nécessité de recompiler chaque fois qu'un détail d'implémentation change. Le meilleur comportement de compilation est en réalité un effet secondaire de l'encapsulation qui est obtenu lorsque l'interface et la mise en œuvre sont séparées. Retourner (et transmettre, attribuer) par valeur nécessite les informations d'implémentation.
Peter - Réintégrer Monica
12

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, fopenrenvoie une seule donnée (la donnée ouverte FILE*) et, en cas d'erreur, donne le code d'erreur via la errnovariable pseudo-globale. Mais il serait peut-être préférable de renvoyer un structdes deux membres: le FILE*descripteur et le code d'erreur (qui serait défini si le descripteur de fichier est NULL). Pour des raisons historiques, ce n'est pas le cas (et les erreurs sont signalées via le errnoglobal, 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 structscalaire 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 structpeut être plus lisible, plus convivial aux threads et plus efficace.

Basile Starynkevitch
la source
En fait , les petits struct sont emballés dans rdx:rax. So struct foo { int a,b; };est renvoyé dans rax(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.
Peter Cordes
Related: 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 d'une constante de masque 64 bits pour le tester test. Ou vous pourriez utiliser bt. Mais ça craint pour l'appelant et l'appelé comparé à utiliser dl, 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)
Peter Cordes
@PeterCordes: vos liens sont le C ++ et non le C
Basile Starynkevitch
Oups, c'est ça. Eh bien, la même chose s’appliquerait exactement à 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.
Peter Cordes
1
Exemple classiquediv_t div()
chux
6

Vous êtes sur la bonne voie

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

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 pointeur NULL.

Retourner 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 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:

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;
}
Ryan
la source
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 de structure?
Yoyo_fun
9
@JenniferAnderson C a un concept de types incomplets: un nom de type peut être déclaré mais pas encore défini, sa taille est donc indisponible. 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 seulement définir les structures dans un fichier C, permettant ainsi l'encapsulation.
amon
@ amon C’est donc comment déclarer les en-têtes de fonctions (prototypes / signatures) avant de déclarer leur fonctionnement en C? Et il est possible de faire la même chose avec les structures et les syndicats de C
yoyo_fun
@JenniferAnderson vous déclarez des prototypes de fonctions (fonctions sans corps) dans des fichiers d'en-tête et pouvez ensuite appeler ces fonctions dans un autre code, sans connaître le corps des fonctions, car le compilateur a simplement besoin de savoir comment arranger les arguments et accepter les valeur de retour. Au moment où vous liez le programme, vous devez réellement connaître la définition de la fonction (c'est-à-dire avec un corps), mais vous ne devez la traiter qu'une seule fois. Si vous utilisez un type non simple, il doit également connaître la structure de ce type, mais les pointeurs ont souvent la même taille et importent peu pour un prototype.
simpleuser
6

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 appelle fopen, 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- freadci, 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 à freaddoit avoir la possibilité de mettre à jour cette information. Avoir freadreçu un pointeur sur le FILErend aussi facile. Si freadau lieu de cela reçu un FILE, il n'aurait aucun moyen de mettre à jour l' FILEobjet détenu par l'appelant.

supercat
la source
3

Masquage de l'information

Quel est l’avantage de renvoyer un pointeur sur une structure plutôt que de renvoyer l’ensemble de la structure dans l’énoncé de retour de la fonction?

Le plus commun est la dissimulation d'informations . C n’a pas, par exemple, la capacité de créer des champs d’un domaine structprivé, 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 de FILEne sera alors visible que pour ceux qui implémentent les opérations qui nécessitent sa définition, comme fopen, 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é

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

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 fopenrenvoyer un pointeur, car la seule raison de son retour NULLest 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 permettant NULLde 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 fclosene 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 de FILEet en la renvoyant par valeur fopenou 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 mallocet 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:

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);

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:

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 vous sauver la vie si nous découvrons à l'avenir que nous devons ajouter des données supplémentaires Foosans 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:

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 à Foopartir de la pile sans possibilité de copie superflue. Ou le client a même la liberté d'allouer Foosur le tas s'il le souhaite pour une raison quelconque.


la source