Y a-t-il des inconvénients à passer des structures par valeur en C, plutôt que de passer un pointeur?
Si la structure est grande, il y a évidemment l'aspect performant de la copie de beaucoup de données, mais pour une structure plus petite, cela devrait être fondamentalement la même chose que de passer plusieurs valeurs à une fonction.
C'est peut-être encore plus intéressant lorsqu'il est utilisé comme valeur de retour. C n'a qu'une seule valeur de retour des fonctions, mais vous en avez souvent besoin. Une solution simple consiste donc à les mettre dans une structure et à la renvoyer.
Y a-t-il des raisons pour ou contre cela?
Puisque ce dont je parle ici n'est peut-être pas évident pour tout le monde, je vais donner un exemple simple.
Si vous programmez en C, vous commencerez tôt ou tard à écrire des fonctions qui ressemblent à ceci:
void examine_data(const char *ptr, size_t len)
{
...
}
char *p = ...;
size_t l = ...;
examine_data(p, l);
Ce n'est pas un problème. Le seul problème est que vous devez convenir avec votre collègue de l'ordre dans lequel les paramètres doivent être afin d'utiliser la même convention dans toutes les fonctions.
Mais que se passe-t-il lorsque vous souhaitez renvoyer le même type d'informations? Vous obtenez généralement quelque chose comme ceci:
char *get_data(size_t *len);
{
...
*len = ...datalen...;
return ...data...;
}
size_t len;
char *p = get_data(&len);
Cela fonctionne bien, mais est beaucoup plus problématique. Une valeur de retour est une valeur de retour, sauf que dans cette implémentation, ce n'est pas le cas. Il n'y a aucun moyen de dire à partir de ce qui précède que la fonction get_data n'est pas autorisée à regarder vers quoi pointe len. Et rien ne permet au compilateur de vérifier qu'une valeur est effectivement renvoyée via ce pointeur. Donc le mois prochain, quand quelqu'un d'autre modifie le code sans le comprendre correctement (parce qu'il n'a pas lu la documentation?), Il est cassé sans que personne ne s'en aperçoive, ou il commence à planter au hasard.
Donc, la solution que je propose est la structure simple
struct blob { char *ptr; size_t len; }
Les exemples peuvent être réécrits comme ceci:
void examine_data(const struct blob data)
{
... use data.tr and data.len ...
}
struct blob = { .ptr = ..., .len = ... };
examine_data(blob);
struct blob get_data(void);
{
...
return (struct blob){ .ptr = ...data..., .len = ...len... };
}
struct blob data = get_data();
Pour une raison quelconque, je pense que la plupart des gens feraient instinctivement examine_data prendre un pointeur vers un blob struct, mais je ne vois pas pourquoi. Il obtient toujours un pointeur et un entier, il est juste beaucoup plus clair qu'ils vont ensemble. Et dans le cas de get_data, il est impossible de gâcher la manière dont j'ai décrit précédemment, car il n'y a pas de valeur d'entrée pour la longueur, et il doit y avoir une longueur retournée.
la source
void examine data(const struct blob)
c'est incorrect.gettimeofday
) utiliser des pointeurs à la place, et les gens prennent cela comme exemple.Réponses:
Pour les petites structures (par exemple point, rect), le passage par valeur est parfaitement acceptable. Mais, mis à part la vitesse, il y a une autre raison pour laquelle vous devriez faire attention en passant / renvoyant de grandes structures par valeur: l'espace de pile.
Une grande partie de la programmation C est destinée aux systèmes embarqués, où la mémoire est limitée, et les tailles de pile peuvent être mesurées en Ko ou même en octets ... Si vous passez ou retournez des structures par valeur, des copies de ces structures seront placées sur la pile, provoquant potentiellement la situation d'après laquelle ce site est nommé ...
Si je vois une application qui semble avoir une utilisation excessive de la pile, les structures passées par valeur sont l'une des choses que je recherche en premier.
la source
Une raison de ne pas faire cela qui n'a pas été mentionnée est que cela peut causer un problème où la compatibilité binaire est importante.
Selon le compilateur utilisé, les structures peuvent être passées via la pile ou les registres en fonction des options / implémentation du compilateur
Voir: http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html
Si deux compilateurs ne sont pas d'accord, les choses peuvent exploser. Inutile de dire que les principales raisons de ne pas le faire sont illustrées par la consommation de pile et des raisons de performances.
la source
int &bar() { int f; int &j(f); return j;};
Pour vraiment répondre à cette question, il faut creuser profondément dans la terre de rassemblement:
(L'exemple suivant utilise gcc sur x86_64. N'importe qui est invité à ajouter d'autres architectures comme MSVC, ARM, etc.)
Prenons notre exemple de programme:
Compilez-le avec des optimisations complètes
Regardez l'assemblage:
Voici ce que nous obtenons:
À l'exclusion des
nopl
blocs,give_two_doubles()
a 27 octets tandis quegive_point()
a 29 octets. En revanche,give_point()
donne une instruction de moins quegive_two_doubles()
Ce qui est intéressant, c'est que nous remarquons que le compilateur a été en mesure d'optimiser
mov
les variantes plus rapides de SSE2movapd
etmovsd
. De plus,give_two_doubles()
déplace réellement les données vers et depuis la mémoire, ce qui ralentit les choses.Apparemment, une grande partie de cela peut ne pas être applicable dans les environnements embarqués (c'est là que le terrain de jeu pour C est la plupart du temps de nos jours). Je ne suis pas un assistant d'assemblage, donc tout commentaire serait le bienvenu!
la source
const
tout le temps) et j'ai trouvé qu'il n'y a pas beaucoup de pénalité de performance (sinon de gain) dans la copie passe par valeur , contrairement à ce que beaucoup pourraient croire.La solution simple sera de retourner un code d'erreur comme valeur de retour et tout le reste comme paramètre dans la fonction,
ce paramètre peut être une structure bien sûr mais ne voyez aucun avantage particulier en le passant par valeur, il suffit d'envoyer un pointeur.
Passer une structure par valeur est dangereux, vous devez faire très attention à ce que vous passez, rappelez-vous qu'il n'y a pas de constructeur de copie en C, si l'un des paramètres de structure est un pointeur, la valeur du pointeur sera copiée, cela peut être très déroutant et difficile à maintenir.
Juste pour compléter la réponse (crédit complet à Roddy ), l'utilisation de la pile est une autre raison de ne pas passer la structure par valeur, croyez-moi, le débogage du débogage de la pile est un vrai PITA.
Rejouer pour commenter:
Passer une structure par un pointeur signifie qu'une entité détient un droit de propriété sur cet objet et a une connaissance complète de ce qui doit être libéré et du moment. Passer une structure par valeur crée une référence cachée aux données internes de la structure (pointeurs vers une autre structure, etc.) à ce niveau est difficile à maintenir (possible mais pourquoi?).
la source
Une chose que les gens ici ont oublié de mentionner jusqu'à présent (ou je l'ai négligée) est que les structures ont généralement un rembourrage!
Chaque caractère est de 1 octet, chaque court est de 2 octets. Quelle est la taille de la structure? Non, ce n'est pas 6 octets. Du moins pas sur les systèmes les plus couramment utilisés. Sur la plupart des systèmes, ce sera 8. Le problème est que l'alignement n'est pas constant, il dépend du système, donc la même structure aura un alignement différent et des tailles différentes sur différents systèmes.
Non seulement le rembourrage consommera davantage votre pile, mais cela ajoute également l'incertitude de ne pas être en mesure de prédire le remplissage à l'avance, à moins que vous ne sachiez comment votre système se remplit, puis regardez chaque structure que vous avez dans votre application et calculez la taille. pour ça. Passer un pointeur prend une quantité d'espace prévisible - il n'y a pas d'incertitude. La taille d'un pointeur est connue pour le système, elle est toujours égale, quel que soit l'aspect de la structure et les tailles des pointeurs sont toujours choisies de manière à être alignées et ne nécessitent aucun remplissage.
la source
Je pense que votre question résume assez bien les choses.
Un autre avantage du passage des structures par valeur est que la propriété de la mémoire est explicite. On ne se demande pas si la structure provient du tas et à qui incombe la responsabilité de la libérer.
la source
Je dirais que passer des structures (pas trop grandes) par valeur, à la fois en tant que paramètres et en tant que valeurs de retour, est une technique parfaitement légitime. Il faut bien sûr veiller à ce que la structure soit de type POD ou que la sémantique de copie soit bien spécifiée.
Mise à jour: Désolé, j'avais mon cap de réflexion C ++. Je me souviens d'une époque où il n'était pas légal en C de renvoyer une structure à partir d'une fonction, mais cela a probablement changé depuis. Je dirais toujours que c'est valable tant que tous les compilateurs que vous prévoyez d'utiliser prennent en charge la pratique.
la source
Voici quelque chose que personne n'a mentionné:
Les membres de a
const struct
sontconst
, mais si ce membre est un pointeur (commechar *
), il devientchar *const
plutôt que ce queconst char *
nous voulons vraiment. Bien sûr, nous pourrions supposer qu'ilconst
s'agit d'une documentation d'intention et que quiconque enfreint ceci écrit du mauvais code (ce qu'ils sont), mais ce n'est pas suffisant pour certains (en particulier ceux qui viennent de passer quatre heures à rechercher la cause d'un crash).L'alternative pourrait être de faire un
struct const_blob { const char *c; size_t l }
et de l'utiliser, mais c'est plutôt compliqué - cela entre dans le même problème de schéma de nommage que j'ai avectypedef
les pointeurs ing. Ainsi, la plupart des gens s'en tiennent à n'avoir que deux paramètres (ou, plus probablement dans ce cas, à l'aide d'une bibliothèque de chaînes).la source
struct const_blob
solution est que même siconst_blob
a des membres qui diffèrent deblob
seulement par "indirect-const-ness", les typesstruct blob*
à astruct const_blob*
seront considérés comme distincts aux fins d'une règle stricte d'aliasing. Par conséquent, si le code convertit ablob*
en aconst_blob*
, toute écriture ultérieure dans la structure sous-jacente en utilisant un type invalidera silencieusement tous les pointeurs existants de l'autre type, de sorte que toute utilisation invoquera un comportement indéfini (qui peut généralement être inoffensif, mais pourrait être mortel) .La page 150 du tutoriel d'assemblage PC sur http://www.drpaulcarter.com/pcasm/ explique clairement comment C permet à une fonction de renvoyer une structure:
J'utilise le code C suivant pour vérifier l'instruction ci-dessus:
Utilisez "gcc -S" pour générer l'assembly pour ce morceau de code C:
La pile avant l'appel crée:
La pile juste après avoir appelé create:
la source
Je veux juste souligner l'un des avantages de la transmission de vos structures par valeur, c'est qu'un compilateur d'optimisation peut mieux optimiser votre code.
la source