web-dev-qa-db-fra.com

En C, comment choisirais-je de renvoyer une structure ou un pointeur vers une structure?

Travailler sur mon muscle C récemment et parcourir les nombreuses bibliothèques avec lesquelles j'ai travaillé m'a certainement donné une bonne idée de ce qu'est une bonne pratique. Une chose que je n'ai PAS vue est une fonction qui retourne une structure:

something_t make_something() { ... }

D'après 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 beaucoup plus populaire que l'extrait 1. Alors maintenant, je demande, pourquoi devrais-je jamais retourner une structure directement, comme dans l'extrait 1? Quelles différences dois-je prendre en compte lorsque je choisis entre les deux options?

De plus, comment cette option se compare-t-elle?

void make_something(something_t *object)
53
Sanchke Dellowar

Quand something_t est petit (lire: le copier est à peu près aussi bon marché que copier un pointeur) et vous voulez qu'il soit alloué par pile 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();

Quand something_t est grand ou vous voulez qu'il soit alloué en tas:

something_t *make_something(void);

something_t *heap_thing = make_something();

Quelle que soit la taille de something_t, et si vous ne vous souciez pas de la destination:

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);
57
Jon Purdy

Il s'agit presque toujours de la stabilité ABI. Stabilité binaire 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 dynamique. Il s'agit rarement de structs ou de performances extrêmement grandes.


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

Vraiment, la vitesse n'est pas la raison de la technique 2, le retour par pointeur, au lieu du retour par valeur.

La technique 2 existe pour la stabilité ABI. Si vous avez un struct et que votre prochaine version de la bibliothèque lui ajoute 20 autres champs, les consommateurs de votre version précédente de la bibliothèque sont compatibles binaires s'ils reçoivent des pointeurs préconstruits . Les données supplémentaires au-delà de la fin du struct qu'ils connaissent sont quelque chose qu'ils n'ont pas à connaître.

Si vous le renvoyez sur la pile, l'appelant lui alloue la mémoire et il doit être d'accord avec vous sur sa taille. Si votre bibliothèque a été mise à jour depuis la dernière reconstruction, vous allez jeter la pile.

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

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

Donc

something_t make_something(unsigned library_version) { ... }

library_version est utilisé par la bibliothèque pour déterminer la version de something_t on s'attend à ce qu'il revienne et il change la quantité de la pile qu'il manipule. Ce n'est pas possible en utilisant la norme C, mais

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

est. Dans ce cas, something_t pourrait avoir un champ version comme premier élément (ou un champ de taille), et vous auriez besoin qu'il soit rempli avant d'appeler make_something.

Autre code de bibliothèque prenant un something_t interrogerait alors le champ version pour déterminer la version de something_t avec qui ils travaillent.

36

En règle générale, vous ne devez jamais transmettre struct objets par valeur. En pratique, ce sera bien pour autant qu'ils soient plus petits ou égaux à la taille maximale que votre CPU peut gérer en une seule instruction. Mais stylistiquement, on l'évite généralement même alors. Si vous ne transmettez jamais de structures par valeur, vous pouvez ajouter des membres ultérieurement à la structure et cela n'affectera pas les performances.

Je pense que void make_something(something_t *object) est la façon la plus courante d'utiliser les structures en C. Vous laissez l'allocation à l'appelant. C'est efficace mais pas joli.

Cependant, les programmes C orientés objet 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 dynamique ou autre dépend de l'implémentation. OO avec un type opaque est souvent l'un des moyens les plus élégants et les meilleurs pour concevoir des programmes C plus complexes, mais malheureusement, peu de programmeurs C le connaissent/s'en soucient.

13
Lundin

Quelques avantages de la première approche:

  • Moins de code à écrire.
  • Plus idiomatique pour le cas d'utilisation du renvoi de plusieurs valeurs.
  • Fonctionne sur les systèmes qui n'ont pas d'allocation dynamique.
  • Probablement plus rapide pour les objets petits ou petits.
  • Aucune fuite de mémoire due à l'oubli de free.

Quelques inconvénients:

  • Si l'objet est volumineux (disons, un mégaoctet), cela peut provoquer un débordement de pile ou peut être lent si les compilateurs ne l'optimisent pas bien.
  • Peut surprendre les personnes qui ont appris le C dans les années 1970 lorsque cela n'était pas possible et qui ne se sont pas tenues à jour.
  • Ne fonctionne pas avec les objets qui contiennent un pointeur sur une partie d'eux-mêmes.
9
M.M

Je suis un peu surpris.

La différence est que l'exemple 1 crée une structure sur la pile, l'exemple 2 la crée sur le tas. Dans le code C ou C++ qui est en fait C, il est idiomatique et pratique de créer la plupart des objets sur le tas. En C++ ce n'est pas le cas, la plupart du temps ils vont sur la pile. La raison en est que si vous créez un objet sur la pile, 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'y a pas de fuites de mémoire et de gérer les exceptions est tout se passe sur la pile. En C, le destructeur doit de toute façon être appelé explicitement, et il n'y a pas de concept de fonction de destructeur spéciale (vous avez des destructeurs, bien sûr, mais ce ne sont que des fonctions normales avec des noms comme destroy_myobject ()).

Maintenant, l'exception en C++ concerne les objets conteneurs de bas niveau, par exemple vecteurs, arbres, cartes de hachage, etc. Ceux-ci conservent des membres de tas et ont des destructeurs. Maintenant, la plupart des objets gourmands en mémoire se composent de quelques membres de données immédiats donnant des tailles, des identifiants, des balises, etc., puis du reste des informations dans les structures STL, peut-être un vecteur de données de pixels ou une carte de paires mot anglais/valeur. Donc, la plupart des données sont en fait sur le tas, même en C++.

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

class big
{
    std::vector<double> observations; // thousands of observations
    int station_x;                    // a bit of data associated with them
    int station_y; 
    std::string station_name; 
}  

big retrieveobservations(int a, int b, int c)
{
    big answer;
    //  lots of code to fill in the structure here

    return answer;
}

void high_level()
{
   big myobservations = retriveobservations(1, 2, 3);
}

Compilera en code assez efficace. Le grand membre d'observation ne générera pas de copies de création inutiles.

4
Malcolm McLean

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 forme de Tuple, qui sont affectées à x et y.

Étant donné que C n'a pas le concept d'un Tuple, il n'est pas pratique de renvoyer plusieurs valeurs à partir 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ù vous pourriez voir une fonction renvoyer une structure.

3
user3386109