55 votes

Conception de l'API C : Qui doit allouer ?

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

Je vois, dans un premier temps, deux options :

1) Laisser l'appelant faire toute la gestion de la mémoire (externe) :

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

myStruct_foo(s);

myStruct_destroy(s);
free(s);

El _init y _destroy sont nécessaires car de la mémoire supplémentaire peut être allouée à l'intérieur, et elle doit être gérée quelque part.

Cela a l'inconvénient d'être plus long, mais aussi le malloc peut être éliminé dans certains cas (par exemple, on peut lui passer un struct alloué par la pile :

int bar() {
    myStruct s;
    myStruct_init(&s);

    myStruct_foo(&s);

    myStruct_destroy(&s);
}

De plus, il est nécessaire que l'appelant connaisse la taille de la structure.

2) Cacher malloc en _init y free en _destroy .

Avantages : code plus court, puisque les fonctions seront appelées de toute façon. Structures complètement opaques.

Inconvénients : On ne peut pas passer une structure allouée d'une manière différente.

myStruct *s = myStruct_init();

myStruct_foo(s);

myStruct_destroy(foo);

Je penche actuellement pour le premier cas ; mais encore une fois, je ne connais pas la conception des API C.

2 votes

En fait, je pense que ce serait une excellente question d'entretien, pour comparer et contraster les deux modèles.

3 votes

Voici un article d'Armin Ronacher sur la façon de rendre les structures opaques tout en permettant de personnaliser l'allocation : lucumr.pocoo.org/2013/8/18/belles-bibliothèques-natives

19voto

Pavel Minaev Points 60647

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. On peut contourner ce problème en fournissant une API permettant au client d'enregistrer ses propres fonctions d'allocation/désallocation (comme le fait SDL), mais même cela peut ne pas être suffisamment précis.

L'inconvénient de la première fonction est qu'elle ne fonctionne pas bien lorsque les tampons de sortie ne sont pas de taille fixe (par exemple, les chaînes de caractères). Au mieux, vous devrez alors fournir une autre fonction pour obtenir d'abord la longueur du tampon afin que l'appelant puisse l'allouer. Au pire, il est tout simplement impossible de le faire efficacement (c'est-à-dire que le calcul de la longueur sur un chemin séparé est trop coûteux par rapport au calcul et à la copie en une seule fois).

L'avantage du #2 est qu'il vous permet d'exposer votre type de données strictement comme un pointeur opaque (c'est-à-dire déclarer la structure mais ne pas la définir, et utiliser les pointeurs de manière cohérente). Vous pouvez alors modifier la définition de la structure comme bon vous semble dans les versions futures de votre bibliothèque, tout en conservant la compatibilité binaire avec les clients. Avec la version #1, vous devez le faire en demandant au client de spécifier la version à l'intérieur de la structure d'une manière ou d'une autre (par exemple, tous les fichiers cbSize dans l'API Win32), puis écrire manuellement un code capable de gérer les anciennes et les nouvelles versions de la structure afin de rester compatible sur le plan binaire au fur et à mesure de 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, je choisirais la première option. S'il s'agit d'un objet de données plus ou moins compliqué et que vous voulez une encapsulation complète pour le rendre infaillible pour les développements futurs, allez avec le #2.

1 votes

+1 pour le point sur l'abstraction et les pointeurs opaques - c'est un gros avantage car cela découple complètement votre implémentation du code appelant.

0 votes

Belle réponse pour avoir une véritable recommandation de discernement sur le moment d'utiliser chaque méthode.

18voto

JeremyP Points 46808

Méthode numéro 2 à chaque fois.

Pourquoi ? Parce qu'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 la taille de la structure. Vous ne pouvez pas modifier l'implémentation interne de l'objet sans recompiler le code qui l'utilise.

3 votes

Ce qui signifie que le point 2 peut être implémenté comme une interface compatible binaire, avec des ajouts mineurs d'API, des améliorations, etc. qui ne cassent pas le code client lorsqu'ils sont livrés dans un .so ou un .dll Cette réponse nécessite plus de votes positifs.

3 votes

L'appelant doit connaître la taille de l'objet (et peut-être l'alignement ?), mais cela ne signifie pas qu'il doive le connaître statiquement : vous auriez pu myStruct_size(void) y myStruct_alignment(void) . Voir cette question .

0 votes

@Kalrish Pourquoi l'appelant doit-il connaître la taille ? Je suis d'accord que si Vous pouvez ajouter les méthodes que vous suggérez, mais une API correctement conçue ne nécessite pas que l'appelant connaisse quoi que ce soit sur les éléments internes d'un objet, y compris la taille et l'alignement.

12voto

Secure Points 2838

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

Utilisez les fonctions _init et _terminate pour utiliser la méthode #1 (ou toute autre dénomination que vous jugez appropriée).

Utilisez les fonctions supplémentaires _create et _destroy pour l'allocation dynamique. Puisque _init et _terminate existent déjà, cela se résume effectivement à :

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 exposent pas dans l'API, fournissant seulement _create et _destroy. Si vous avez besoin d'autres allocations, par exemple avec un callback donné, fournissez un autre ensemble de fonctions pour cela, par exemple _createcalled, _destroycalled.

L'important est de garder la trace des allocations, mais vous devez le faire de toute façon. Vous devez toujours utiliser la contrepartie de l'allocateur utilisé pour la désallocation.

3 votes

Existe-t-il une bibliothèque C bien connue qui a adopté cette approche ?

0 votes

@cubuspl2 existe-t-il une bibliothèque C ou un auteur connu qui explique pourquoi il n'a pas adopté cette approche ?

10voto

Dean Harding Points 40164

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

Bien qu'un autre avantage de votre méthode #1 ne soit pas seulement que vous puissiez allouer l'objet sur la pile, mais aussi que vous puissiez réutiliser la même instance plusieurs fois. Si ce n'est pas un cas d'utilisation courant, alors la simplicité du point 2 est probablement un avantage.

Bien sûr, c'est juste mon opinion :)

0 votes

C'est un commentaire intéressant. J'ai entendu beaucoup de gens dire exactement le contraire, que GTK+ est une API terrible. Je ne l'ai malheureusement que peu utilisé, je suis habituellement dans les nuages du C++ et j'utilise Gtkmm. Mon expérience se souvient des pointeurs ref-countés, et des fonctions _new et _free, cependant, ce qui semble correspondre plus à la 3ème option. Je serais curieux de connaître les raisons de votre opinion.

2 votes

La philosophie générale de conception de GLib/Gtk semble être "nous n'utiliserons pas C++ par principe, donc nous coderons à la main toutes les mêmes choses". Cette approche présente quelques avantages dans le sens où il s'agit toujours d'une API purement C, ce qui la rend plus facile à utiliser avec diverses FFI exclusivement C... mais d'un point de vue purement C/C++, elle semble plutôt peu pratique.

4voto

341008 Points 2317

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

  1. C'est plus intuitif. Pourquoi devrais-je appeler free sur l'objet après que je l'ai (apparemment) détruit en utilisant myStruct_Destroy .

  2. Cache les détails de myStruct de l'utilisateur. Il n'a pas à se soucier de sa taille, etc.

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

  4. Vous n'avez pas à vous soucier des fuites de mémoire dues au fait que l'utilisateur a oublié d'appeler free .

Toutefois, si l'implémentation de votre API est expédiée sous la forme d'une bibliothèque partagée distincte, la méthode n° 2 est indispensable. Pour isoler votre module de tout décalage dans les implémentations de malloc / new y free / delete entre les versions du compilateur, vous devez garder pour vous l'allocation et la désallocation de la mémoire. Notez que ceci est plus vrai pour le C++ que pour le C.

1 votes

Les deux sont pas équivalent, car le second requiert une allocation dynamique, et le premier non.

0 votes

Eh bien... oui. J'aurais dû dire "fonctionnellement équivalent". Mis à jour.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X