Conception de l’API C: Qui devrait allouer?

Quelle est la manière appropriée / préférée d’allouer de la mémoire dans une API C?

Je peux d’abord voir deux options:

1) Laisser l’appelant faire tout le traitement de la mémoire (externe):

myStruct *s = malloc(sizeof(s)); myStruct_init(s); myStruct_foo(s); myStruct_destroy(s); free(s); 

Les fonctions _init et _destroy sont nécessaires car un peu plus de mémoire peut être allouée à l’intérieur, et il doit être manipulé quelque part.

Cela présente l’inconvénient d’être plus long, mais le malloc peut également être éliminé dans certains cas (par exemple, il peut être passé à une structure allouée à la stack:

 int bar() { myStruct s; myStruct_init(&s); myStruct_foo(&s); myStruct_destroy(&s); } 

En outre, l’appelant doit connaître la taille de la structure.

2) Cacher malloc s dans _init et free s dans _destroy .

Avantages: code plus court, car les fonctions vont être appelées de toute façon. Structures complètement opaques

Inconvénients: Impossible de transmettre une structure allouée différemment.

 myStruct *s = myStruct_init(); myStruct_foo(s); myStruct_destroy(foo); 

Je me penche actuellement pour le premier cas; là encore, je ne connais pas la conception de l’API C.

Mon exemple préféré d’une API C bien conçue est GTK + qui utilise la méthode n ° 2 que vous décrivez.

Bien qu’un autre avantage de votre méthode n ° 1 ne soit pas simplement que vous puissiez allouer l’object sur la stack, mais également que vous puissiez réutiliser la même instance plusieurs fois. Si cela ne va pas être un cas d’utilisation courant, la simplicité de # 2 est probablement un avantage.

Bien sûr, c’est juste mon avis 🙂

Méthode numéro 2 à chaque fois.

Pourquoi? car avec la méthode numéro 1, vous devez divulguer les détails de l’implémentation à l’appelant. L’appelant doit savoir au moins quelle est la taille de la structure. Vous ne pouvez pas modifier l’implémentation interne de l’object sans recomstackr le code qui l’utilise.

Un autre inconvénient de # 2 est que l’appelant n’a pas le contrôle sur la façon dont les choses sont allouées. Cela peut être contourné en fournissant une API pour que le client enregistre ses propres fonctions d’allocation / de désallocation (comme le fait SDL), mais même cela peut ne pas être suffisamment fin.

L’inconvénient de # 1 est qu’il ne fonctionne pas bien lorsque les tampons de sortie ne sont pas de taille fixe (par exemple, les chaînes). Au mieux, vous devrez alors fournir une autre fonction pour obtenir d’abord la longueur du tampon afin que l’appelant puisse l’allouer. Dans le pire des cas, il est tout simplement impossible de le faire efficacement (c’est-à-dire que la longueur de calcul sur un chemin distinct est trop onéreuse par rapport au calcul et à la copie en une fois).

L’avantage de # 2 est qu’il vous permet d’exposer votre type de données ssortingctement en tant que pointeur opaque (c’est-à-dire de déclarer la structure mais de ne pas la définir et d’utiliser des pointeurs de manière cohérente). Ensuite, vous pouvez modifier la définition de la structure comme vous le voyez dans les futures versions de votre bibliothèque, tandis que les clients restnt compatibles au niveau binary. Avec # 1, vous devez le faire en demandant au client de spécifier la version à l’intérieur de la structure (par exemple tous les champs cbSize dans l’API Win32), puis écrivez manuellement le code capable de gérer les versions plus anciennes et plus récentes de la structure. restr binary compatible avec l’évolution de votre bibliothèque.

En général, si vos structures sont des données transparentes qui ne changeront pas avec les futures révisions mineures de la bibliothèque, j’irais avec # 1. Si c’est un object de données plus ou moins compliqué et que vous souhaitez une encapsulation complète pour le protéger contre les développements futurs, optez pour # 2.

Pourquoi ne pas fournir les deux, pour obtenir le meilleur des deux mondes?

Utilisez les fonctions _init et _terminate pour utiliser la méthode n ° 1 (ou tout autre nom qui vous convient).

Utilisez des fonctions _create et _destroy supplémentaires pour l’allocation dynamic. Comme _init et _terminate existent déjà, cela se résume à:

 myStruct *myStruct_create () { myStruct *s = malloc(sizeof(*s)); if (s) { myStruct_init(s); } return (s); } void myStruct_destroy (myStruct *s) { myStruct_terminate(s); free(s); } 

Si vous voulez qu’il soit opaque, alors faites _init et _terminate static et ne les exposez pas dans l’API, fournissez seulement _create et _destroy. Si vous avez besoin d’autres atsortingbutions, par exemple avec un rappel donné, fournissez un autre ensemble de fonctions pour cela, par exemple _createcalled, _destroycalled.

L’important est de garder une trace des allocations, mais vous devez le faire quand même. Vous devez toujours utiliser la contrepartie de l’allocateur utilisé pour la désallocation.

Les deux sont fonctionnellement équivalents. Mais, à mon avis, la méthode n ° 2 est plus facile à utiliser. Quelques raisons de préférer 2 sur 1 sont:

  1. C’est plus intuitif. Pourquoi devrais-je appeler free sur l’object après l’avoir (apparemment) détruit en utilisant myStruct_Destroy .

  2. Masque les détails de myStruct auprès de l’utilisateur. Il n’a pas à s’inquiéter de sa taille, etc.

  3. Dans la méthode n ° 2, myStruct_init n’a pas à se soucier de l’état initial de l’object.

  4. Vous n’avez pas à vous soucier des memory leaks de l’utilisateur en oubliant d’appeler free .

Si l’implémentation de votre API est livrée en tant que bibliothèque partagée distincte, la méthode n ° 2 est indispensable. Pour isoler votre module de toute incompatibilité dans les implémentations de malloc / new et de free / delete entre les versions du compilateur, vous devez conserver l’allocation de mémoire et la désallocation pour vous-même. Notez que ceci est plus vrai de C ++ que de C.

Le problème que je rencontre avec la première méthode n’est pas tant le fait qu’il est plus long pour l’appelant, mais que l’API est maintenant menottée pour pouvoir augmenter la quantité de mémoire utilisée précisément parce qu’elle ne sait pas comment la mémoire est utilisée. reçu a été alloué. L’appelant ne sait pas toujours à l’avance combien de mémoire il aura besoin (imaginez si vous essayiez d’implémenter un vecteur).

Une autre option que vous n’avez pas mentionnée, qui sera souvent exagérée, consiste à transmettre un pointeur de fonction utilisé par l’API en tant qu’allocateur. Cela ne vous permet pas d’utiliser la stack, mais vous permet de faire quelque chose comme remplacer l’utilisation de malloc par un pool de mémoire, ce qui permet toujours à l’API de contrôler le moment où elle souhaite allouer.

En ce qui concerne la méthode de conception de l’API, elle est utilisée dans les deux sens dans la bibliothèque standard. strdup () et stdio utilisent la seconde méthode, alors que sprintf et strcat utilisent la première méthode. Personnellement, je préfère la deuxième méthode (ou la troisième) à moins que 1) Je sais que je n’aurai jamais besoin de réallouer et 2) J’espère que la durée de vie de mes objects sera courte et que l’utilisation de la stack est très pratique

edit: Il y a en fait 1 autre option, et c’est une mauvaise option avec un précédent important. Vous pouvez le faire comme le fait strtok () avec la statique. Pas bon, juste mentionné pour être complet.

Les deux manières sont correctes, j’ai tendance à faire la première manière comme beaucoup de CI le font pour les systèmes embarqués et toute la mémoire est soit de minuscules variables sur la stack, soit allouées statiquement. De cette façon, il ne peut y avoir de mémoire insuffisante, que vous en ayez suffisamment au début ou que vous soyez foutu dès le départ. Bon à savoir quand vous avez 2K de Ram 🙂 Donc, toutes mes bibliothèques sont comme # 1 où la mémoire est supposée être allouée.

Mais c’est un cas extrême de développement en C.

Cela dit, je vais probablement aller avec # 1 encore. Peut-être en utilisant init et finalize / dispose (plutôt que de détruire) pour les noms.

Cela pourrait donner un élément de reflection:

le cas n ° 1 imite le schéma d’allocation de mémoire de C ++, avec plus ou moins les mêmes avantages:

  • atsortingbution facile de provisoires sur stack (ou dans des tableaux statiques ou pour écrire votre propre structure allocator en remplacement de malloc).
  • facile à libérer de la mémoire si quelque chose ne va pas dans init

le cas n ° 2 cache davantage d’informations sur la structure utilisée et peut également être utilisé pour les structures opaques, généralement lorsque la structure vue par l’utilisateur n’est pas exactement la même que celle utilisée en interne (par exemple, d’autres champs peuvent être masqués à la fin de la structure) ).

L’API mixte entre le cas n ° 1 et le cas n ° 2 est également courante: il existe un champ permettant de passer un pointeur à une structure déjà initialisée, si elle est nulle, qu’elle est allouée (et le pointeur est toujours renvoyé). Avec une telle API, le libre est généralement la responsabilité de l’appelant même si init a effectué l’allocation.

Dans la plupart des cas, j’irais probablement pour le cas n ° 1.

Les deux sont acceptables – il y a des compromis entre eux, comme vous l’avez noté.

Il existe de nombreux exemples concrets de tous les deux – comme le dit Dean Harding , GTK + utilise la deuxième méthode; OpenSSL est un exemple qui utilise le premier.

Je voudrais aller (1) avec une simple extension, c’est-à-dire que votre fonction _init toujours le pointeur sur l’object. L’initialisation de votre pointeur peut alors simplement lire:

 myStruct *s = myStruct_init(malloc(sizeof(myStruct))); 

Comme vous pouvez le voir, la partie droite ne fait plus référence qu’au type et non à la variable. Une simple macro vous donne alors (2) au moins partiellement

 #define NEW(T) (T ## _init(malloc(sizeof(T)))) 

et l’initialisation de votre pointeur lit

 myStruct *s = NEW(myStruct); 

Voir votre méthode # 2 dit

 myStruct *s = myStruct_init(); myStruct_foo(s); myStruct_destroy(s); 

Maintenant, voyez si myStruct_init() besoin de retourner un code d’erreur pour diverses raisons, alors allons-y.

 myStruct *s; int ret = myStruct_init(&s); // int myStruct_init(myStruct **s); myStruct_foo(s); myStruct_destroy(s);