Est-il mauvais d'écrire en C orienté objet? [fermé]

14

Il me semble toujours écrire du code en C qui est principalement orienté objet, alors disons que j'avais un fichier source ou quelque chose que je créerais une structure puis passerais le pointeur vers cette structure aux fonctions (méthodes) appartenant à cette structure:

struct foo {
    int x;
};

struct foo* createFoo(); // mallocs foo

void destroyFoo(struct foo* foo); // frees foo and its things

Est-ce une mauvaise pratique? Comment puis-je apprendre à écrire C de la "bonne façon".

mosmo
la source
10
Une grande partie de Linux (le noyau) est écrit de cette façon, en fait, il émule même plus de concepts de type OO comme la répartition de méthode virtuelle. Je considère cela comme assez approprié.
Kilian Foth du
13
« [L] a déterminé programmeur réel peut écrire des programmes FORTRAN dans toutes les langues. » - Ed Post, 1983
Ross Patterson
4
Y a-t-il une raison pour laquelle vous ne voulez pas passer en C ++? Vous n'êtes pas obligé d'en utiliser les parties que vous n'aimez pas.
svick
5
Cela soulève vraiment la question: «Qu'est-ce qui est« orienté objet »?» Je n'appellerais pas cet objet orienté. Je dirais que c'est procédural. (Vous n'avez pas d'héritage, pas de polymorphisme, pas d'encapsulation / capacité à cacher l'état, et il manque probablement d'autres caractéristiques d'OO qui ne me viennent pas du haut de la tête.) Que ce soit une bonne ou une mauvaise pratique ne dépend pas de cette sémantique , bien que.
jpmc26
3
@ jpmc26: Si vous êtes un prescriptiviste linguistique, vous devriez écouter Alan Kay, il a inventé le terme, il arrive à dire ce qu'il signifie, et il dit que la POO est tout au sujet de la messagerie . Si vous êtes un descriptiviste linguistique, vous enquêterez sur l'utilisation du terme dans la communauté du développement logiciel. Cook a fait exactement cela, il a analysé les caractéristiques des langues qui prétendent ou sont considérées comme OO, et il a constaté qu'elles avaient une chose en commun: la messagerie .
Jörg W Mittag

Réponses:

24

Non, ce n'est pas une mauvaise pratique, c'est même encouragé à le faire, même si on pourrait même utiliser des conventions comme struct foo *foo_new();etvoid foo_free(struct foo *foo);

Bien sûr, comme le dit un commentaire, ne faites cela que lorsque cela est approprié. Cela n'a aucun sens d'utiliser un constructeur pour un int.

Le préfixe foo_est une convention suivie par de nombreuses bibliothèques, car il protège contre les conflits avec la dénomination d'autres bibliothèques. D'autres fonctions ont souvent la convention à utiliser foo_<function>(struct foo *foo, <parameters>);. Cela vous permet struct food'être un type opaque.

Jetez un œil à la documentation de libcurl pour la convention, en particulier avec les "subnamespaces", de sorte que l'appel d'une fonction curl_multi_*semble incorrect à première vue lorsque le premier paramètre a été renvoyé par curl_easy_init().

Il existe des approches encore plus génériques, voir Programmation orientée objet avec ANSI-C

Résidu
la source
11
Toujours avec la mise en garde «le cas échéant». La POO n'est pas une solution miracle.
Déduplicateur
C n'a-t-il pas d'espaces de noms dans lesquels vous pouvez déclarer ces fonctions? Similaire à std::string, ne pourriez-vous pas avoir foo::create? Je n'utilise pas C. Peut-être que c'est seulement en C ++?
Chris Cirefice
@ChrisCirefice Il n'y a pas d'espaces de noms en C, c'est pourquoi de nombreux auteurs de bibliothèques utilisent des préfixes pour leurs fonctions.
Residuum
2

Ce n'est pas mal, c'est excellent. La programmation orientée objet est une bonne chose (à moins que vous ne vous emportiez, vous pouvez avoir trop d'une bonne chose). C n'est pas le langage le plus approprié pour la POO, mais cela ne devrait pas vous empêcher d'en tirer le meilleur parti.

gnasher729
la source
4
Non pas que je puisse être en désaccord, mais votre opinion devrait vraiment être étayée par une certaine élaboration.
Déduplicateur
1

Ce n'est pas mauvais. Il approuve l'utilisation de RAII qui empêche de nombreux bugs (fuites de mémoire, utilisation de variables non initialisées, utilisation après libération, etc., ce qui peut entraîner des problèmes de sécurité).

Donc, si vous souhaitez compiler votre code uniquement avec GCC ou Clang (et non avec le compilateur MS), vous pouvez utiliser un cleanupattribut, qui détruira correctement vos objets. Si vous déclarez votre objet comme ça:

my_str __attribute__((cleanup(my_str_destructor))) ptr;

Puis my_str_destructor(ptr)sera exécuté lorsque ptr sortira du cadre. Gardez juste à l'esprit qu'il ne peut pas être utilisé avec des arguments de fonction .

N'oubliez pas non plus d'utiliser my_str_dans vos noms de méthode, car il Cn'a pas d'espaces de noms, et il est facile d'entrer en collision avec un autre nom de fonction.

Marqin
la source
2
Afaik, RAII consiste à utiliser l'appel implicite de destructeurs pour les objets en C ++ pour assurer le nettoyage, en évitant d'avoir à ajouter des appels de libération de ressources explicites. Donc, si je ne me trompe pas beaucoup, RAII et C s'excluent mutuellement.
cmaster - réintègre monica le
@cmaster Si vous #defineutilisez vos noms de caractères, __attribute__((cleanup(my_str_destructor)))vous les obtiendrez comme implicites dans toute la #defineportée (ils seront ajoutés à toutes vos déclarations de variables).
Marqin
Cela fonctionne si a) vous utilisez gcc, b) si vous utilisez le type uniquement dans les variables locales de fonction, et c) si vous utilisez le type uniquement dans une version nue (pas de pointeurs vers le #definetype 'd ou de tableaux de celui-ci). En bref: ce n'est pas du C standard, et vous payez avec beaucoup de rigidité à l'usage.
cmaster - réintègre monica le
Comme mentionné dans ma réponse, cela fonctionne également en clang.
Marqin
Ah, je ne l'ai pas remarqué. Cela rend en effet l'exigence a) beaucoup moins sévère, car cela fait à __attribute__((cleanup()))peu près une quasi-norme. Cependant, b) et c) sont toujours
valables
-2

Il peut y avoir de nombreux avantages à ce code, mais malheureusement, la norme C n'a pas été écrite pour le faciliter. Les compilateurs ont historiquement offert des garanties de comportement efficaces au-delà de ce que la norme exigeait qui permettait d'écrire un tel code beaucoup plus proprement que ce qui est possible dans la norme C, mais les compilateurs ont récemment commencé à révoquer ces garanties au nom de l'optimisation.

Plus particulièrement, de nombreux compilateurs C ont historiquement garanti (par conception sinon documentation) que si deux types de structure contiennent la même séquence initiale, un pointeur vers l'un ou l'autre type peut être utilisé pour accéder aux membres de cette séquence commune, même si les types ne sont pas liés, et en outre qu'aux fins de l'établissement d'une séquence initiale commune, tous les pointeurs vers les structures sont équivalents. Le code qui utilise un tel comportement peut être beaucoup plus propre et plus sûr que le code qui ne le fait pas, mais malheureusement, même si la norme exige que les structures partageant une séquence initiale commune doivent être disposées de la même manière, il interdit au code d'utiliser réellement un pointeur d'un type pour accéder à la séquence initiale d'un autre.

Par conséquent, si vous voulez écrire du code orienté objet en C, vous devrez décider (et devriez prendre cette décision tôt) de sauter à travers un grand nombre de cerceaux pour respecter les règles de type pointeur de C et être prêt à avoir les compilateurs modernes génèrent du code insensé si l'un d'eux glisse, même si des compilateurs plus anciens auraient généré du code qui fonctionne comme prévu, ou bien documentent une exigence selon laquelle le code ne sera utilisable qu'avec des compilateurs configurés pour prendre en charge le comportement de pointeur à l'ancienne (par exemple en utilisant un "-fno-strict-aliasing") Certaines personnes considèrent "-fno-strict-aliasing" comme mauvais, mais je dirais qu'il est plus utile de penser à "-fno-strict-aliasing" C comme étant un langage qui offre une puissance sémantique supérieure à certaines fins que le C "standard",mais au détriment des optimisations qui pourraient être importantes à d'autres fins.

À titre d'exemple, sur les compilateurs traditionnels, les compilateurs historiques interpréteraient le code suivant:

struct pair { int i1,i2; };
struct trio { int i1,i2,i3; };

void hey(struct pair *p, struct trio *t)
{
  p->i1++;
  t->i1^=1;
  p->i1--;
  t->i1^=1;
}

comme effectuer les étapes suivantes dans l'ordre: incrémenter le premier membre de *p, compléter le bit le plus bas du premier membre de *t, puis décrémenter le premier membre de *p, et compléter le bit le plus bas du premier membre de *t. Les compilateurs modernes réorganiseront la séquence des opérations de manière à coder qui sera plus efficace pet tidentifiera les différents objets, mais changeront le comportement s'ils ne le font pas.

Cet exemple est bien sûr délibérément conçu, et dans la pratique, le code qui utilise un pointeur d'un type pour accéder aux membres qui font partie de la séquence initiale commune d'un autre type fonctionnera généralement , mais malheureusement puisqu'il n'y a aucun moyen de savoir quand un tel code pourrait échouer. il n'est pas possible de l'utiliser en toute sécurité, sauf en désactivant l'analyse d'alias basée sur le type.

Un exemple un peu moins artificiel se produirait si l'on voulait écrire une fonction pour faire quelque chose comme échanger deux pointeurs vers des types arbitraires. Dans la grande majorité des compilateurs «C des années 90», cela pourrait être accompli via:

void swap_pointers(void **p1, void **p2)
{
  void *temp = *p1;
  *p1 = *p2;
  *p2 = temp;
}

Dans la norme C, cependant, il faudrait utiliser:

#include "string.h"
#include "stdlib.h"
void swap_pointers2(void **p1, void **p2)
{
  void **temp = malloc(sizeof (void*));
  memcpy(temp, p1, sizeof (void*));
  memcpy(p1, p2, sizeof (void*));
  memcpy(p2, temp, sizeof (void*));
  free(temp);
}

Si *p2est conservé dans le stockage alloué et que le pointeur temporaire n'est pas conservé dans le stockage alloué, le type effectif de *p2deviendra le type du pointeur temporaire et le code qui tentera de l'utiliser *p2comme n'importe quel type ne correspondant pas au pointeur temporaire type invoquera le comportement indéfini. Il est certain qu'il est extrêmement improbable qu'un compilateur remarque une telle chose, mais comme la philosophie moderne du compilateur exige que les programmeurs évitent à tout prix les comportements indéfinis, je ne peux penser à aucun autre moyen sûr d'écrire le code ci-dessus sans utiliser le stockage alloué. .

supercat
la source
Downvoters: Voulez-vous commenter? Un aspect clé de la programmation orientée objet est la possibilité d'avoir plusieurs types partageant des aspects communs, et d'avoir un pointeur vers un tel type utilisable pour accéder à ces aspects communs. L'exemple de l'OP ne fait pas cela, mais il gratte à peine la surface d'être "orienté objet". Les compilateurs C historiques permettraient d'écrire du code polymorphe d'une manière beaucoup plus propre que ce qui est possible dans le standard C. Aujourd'hui, la conception de code orienté objet en C nécessite donc de déterminer le langage exact que l'on cible. Avec quel aspect les gens ne sont pas d'accord?
supercat
Hm ... vous montrez comment les garanties de la norme ne vous permettent pas d'accéder proprement aux membres de la sous-séquence initiale commune? Parce que je pense que c'est ce que votre diatribe sur les maux de l'audace d'optimiser dans les limites du comportement contractuel dépend de cette époque? (C'est ce que je suppose ce que les deux downvoters ont trouvé répréhensible.)
Déduplicateur
La POO ne nécessite pas nécessairement d'héritage, donc la compatibilité entre deux structures n'est pas vraiment un problème dans la pratique. Je peux obtenir un véritable OO en plaçant des pointeurs de fonction dans une structure et en invoquant ces fonctions d'une manière particulière. Bien sûr, ce foo_function(foo*, ...)pseudo-OO en C n'est qu'un style d'API particulier qui ressemble à des classes, mais il devrait être plus correctement appelé programmation modulaire avec des types de données abstraits.
amon
@Deduplicator: Voir l'exemple indiqué. Le champ "i1" est un membre de la séquence initiale commune des deux structures, mais les compilateurs modernes échoueront si le code essaie d'utiliser une "paire de struct *" pour accéder au membre initial d'un "trio de struct".
supercat
Quel compilateur C moderne échoue cet exemple? Avez-vous besoin d'options intéressantes?
Déduplicateur
-3

L'étape suivante consiste à masquer la déclaration struct. Vous mettez ceci dans le fichier .h:

typedef struct foo_s foo_t;

foo_t * foo_new(...);
void foo_destroy(foo_t *foo);
some_type foo_whatever(foo_t *foo, ...);
...

Et puis dans le fichier .c:

struct foo_s {
    ...
};
Solomon Slow
la source
6
Cela pourrait être la prochaine étape, selon l'objectif. Que ce soit ou non, cela n'essaie même pas à distance de répondre à la question.
Déduplicateur