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)
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);
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 struct
s 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 struct
s 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) { ... }
où 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.
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.
Quelques avantages de la première approche:
free
.Quelques inconvénients:
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.
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.