web-dev-qa-db-fra.com

Que faisaient les gens avant les modèles en C ++?

Je ne suis pas nouveau dans la programmation, mais j'en suis un qui a commencé il y a quelques années et j'adore les modèles.

Mais dans les temps précédents, comment les gens faisaient-ils face aux situations où ils avaient besoin de génération de code au moment de la compilation comme des modèles? Je devine des macros horribles et horribles (du moins c'est comme ça que je le ferais), mais googler la question ci-dessus ne me donne que des pages et des pages de tutoriels de modèle.

Il existe de nombreux arguments contre l'utilisation de modèles, et bien que cela se résume généralement à la lisibilité, " YAGNI ", et se plaindre de la mauvaise mise en œuvre, il n'y a pas beaucoup là-bas sur les alternatives avec une puissance similaire. Quand je ai besoin de faire une sorte de générique au moment de la compilation et fais veux garder mon code SEC , comment évite/a-t-on évité d'utiliser des modèles?

44
IdeaHat

Outre le pointeur void * qui est couvert dans la réponse de Robert , une technique comme celle-ci a été utilisée (Avertissement: mémoire de 20 ans):

#define WANTIMP

#define TYPE int
#include "collection.h"
#undef TYPE

#define TYPE string
#include "collection.h"
#undef TYPE

int main() {
    Collection_int lstInt;
    Collection_string lstString;
}

Où j'ai oublié la magie exacte du préprocesseur dans collection.h, mais c'était quelque chose comme ceci:

class Collection_ ## TYPE {
public:
 Collection() {}
 void Add(TYPE value);
private:
 TYPE *list;
 size_t n;
 size_t a;
}

#ifdef WANTIMP
void Collection_ ## TYPE ::Add(TYPE value)
#endif
52
Joshua

La manière traditionnelle d'implémenter des génériques sans avoir de génériques (la raison pour laquelle les modèles ont été créés) est d'utiliser un pointeur vide.

typedef struct Item{ 
        void* data;
    } Item;

typedef struct Node{
    Item Item; 
    struct Node* next; 
    struct Node* previous;
} Node;

Dans cet exemple de code, un arbre binaire ou une liste à double liaison peut être représenté. Étant donné que item encapsule un pointeur vide, tout type de données peut y être stocké. Bien sûr, vous devez connaître le type de données lors de l'exécution afin de pouvoir le restituer en un objet utilisable.

46
Robert Harvey

Comme d'autres réponses l'ont souligné, vous pouvez utiliser void* pour les structures de données génériques. Pour d'autres types de polymorphisme paramétrique, des macros de préprocesseur étaient utilisées si quelque chose se répétait beaucoup (comme des dizaines de fois). Pour être honnête, cependant, la plupart du temps pour une répétition modérée, les gens copient et collent simplement, puis changent de type, car il existe de nombreux pièges avec des macros qui les rendent problématiques.

Nous avons vraiment besoin d'un nom pour l'inverse du blub paradox , où les gens ont du mal à imaginer une programmation dans un langage moins expressif, car cela revient beaucoup sur ce site. Si vous n'avez jamais utilisé un langage avec des moyens expressifs d'implémenter le polymorphisme paramétrique, vous ne savez pas vraiment ce qui vous manque. Vous acceptez en quelque sorte le copier-coller comme quelque peu ennuyeux, mais nécessaire.

Il y a des inefficacités dans vos langues de choix actuelles que vous ne connaissez même pas encore. Dans vingt ans, les gens se demanderont comment vous les avez éliminés. La réponse courte est que vous ne l'avez pas fait, parce que vous ne saviez pas que vous pouviez.

18
Karl Bielefeldt

Je me souviens quand gcc était livré avec genclass - un programme qui prenait en entrée un ensemble de types de paramètres (par exemple clé et valeur pour une carte) et un fichier de syntaxe spécial qui décrivait un type paramétré (disons, une carte ou un Vector) et a généré une implémentation C++ valide avec les types param remplis.

Donc, si vous aviez besoin de Map<int, string> et Map<string, string> (ce n'était pas la syntaxe réelle, sachez que) vous avez dû exécuter ce programme deux fois pour générer quelque chose comme map_string_string.h et map_int_string.h, puis les utiliser dans votre code.

Voici la page de manuel pour genclass: http://manpages.ubuntu.com/manpages/dapper/man1/genclass.1.html

9
Rafał Dowgird

[À l'OP: je n'essaie pas de vous harceler personnellement, mais de sensibiliser vous et les autres à réfléchir à la logique des questions posées sur SE et ailleurs. Veuillez ne pas prendre cela personnellement!]

Le titre de la question est bon, mais vous limitez considérablement la portée de vos réponses en incluant "... les situations où elles avaient besoin de générer du code au moment de la compilation". De nombreuses bonnes réponses à la question sur la façon de générer du code à la compilation en C++ sans modèles existent sur cette page, mais pour répondre à la question que vous avez posée à l'origine:

Que faisaient les gens avant les modèles en C++?

La réponse est, bien sûr, qu'ils (nous) ne les avons pas utilisés. Oui, je suis ironique, mais les détails de la question dans le corps semblent (peut-être exagérément) supposer que tout le monde aime les modèles et qu'aucun codage n'aurait jamais pu être fait sans eux.

Par exemple, j'ai réalisé de nombreux projets de codage dans plusieurs langues sans avoir besoin de générer du code au moment de la compilation, et je pense que d'autres l'ont également fait. Bien sûr, le problème résolu par les modèles était une démangeaison suffisamment grande pour que quelqu'un le gratte, mais le scénario posé par cette question était, en grande partie, inexistant.

Considérez une question similaire dans les voitures:

Comment les conducteurs passaient-ils d'un rapport à l'autre, en utilisant une méthode automatisée qui passait les vitesses pour vous, avant que la transmission automatique ne soit inventée?

La question est, bien sûr, idiote. Demander comment une personne a fait X avant d'inventer X n'est pas vraiment une question valable. La réponse est généralement: "nous ne l'avons pas fait et nous ne l'avons pas manqué parce que nous ne savions pas que cela existerait". Oui, il est facile de voir les avantages après coup, mais supposer que tout le monde se tenait debout, donnait des coups de pied, attendait une transmission automatique ou des modèles C++, n'est vraiment pas vrai.

À la question: "comment les conducteurs ont-ils changé de vitesse avant que la transmission automatique ne soit inventée? on peut raisonnablement répondre, "manuellement", et c'est le type de réponses que vous obtenez ici. Il peut même s'agir du type de question que vous vouliez poser.

Mais ce n'est pas celui que vous avez demandé.

Donc:

Q: Comment les gens utilisaient-ils les modèles avant leur création?

R: Nous ne l'avons pas fait.

Q: Comment les gens utilisaient-ils les modèles avant leur création, quand ils avaient besoin d'utiliser des modèles?

R: Nous ne l'avons pas devons les utiliser. Pourquoi supposer que nous l'avons fait? (Pourquoi supposer que nous le faisons?)

Q: Quels sont les autres moyens d'obtenir les résultats fournis par les modèles?

R: De nombreuses bonnes réponses existent ci-dessus.

S'il vous plaît, pensez aux erreurs logiques dans vos messages avant de publier.

[Je vous remercie! Je vous en prie, aucun mal ici.]

7
Joseph Cheek

Des macros horribles ont raison, de http://www.artima.com/intv/modern2.html :

Bjarne Stroustrup: Oui. Lorsque vous dites "modèle de type T", c'est vraiment l'ancien mathématique "pour tous les T." C'est ainsi que cela est envisagé. Mon tout premier article sur "C avec les classes" (qui a évolué en C++) de 1981 a mentionné les types paramétrés. Là, j'ai bien résolu le problème, mais la solution est totalement fausse. J'ai expliqué comment vous pouvez paramétrer les types avec des macros, et mon garçon c'était du code moche.

Vous pouvez voir comment une ancienne version macro d'un modèle a été utilisée ici: http://www.xvt.com/sites/default/files/docs/Pwr++_Reference/rw/docs/html/toolsref/ rwgvector.html

6
jas

Comme Robert Harvey l'a déjà dit, un pointeur vide est le type de données générique.

Un exemple de la bibliothèque C standard, comment trier un tableau de double avec un tri générique:

double *array = ...;
int size = ...;   
qsort (array, size, sizeof (double), compare_doubles);

compare_double est défini comme:

int compare_doubles (const void *a, const void *b)
{
    const double *da = (const double *) a;
    const double *db = (const double *) b;
    return (*da > *db) - (*da < *db);
}

La signature de qsort est définie dans stdlib.h:

void qsort(void *base, size_t nmemb, size_t size,
    int (*compar)(const void *, const void *)
);

Notez qu'il n'y a pas de vérification de type au moment de la compilation, pas même au moment de l'exécution. Si vous triez une liste de chaînes avec le comparateur ci-dessus qui attend des doubles, il essaiera avec plaisir d'interpréter la représentation binaire d'une chaîne comme un double et de trier en conséquence.

5
Florian F

Une façon de procéder est la suivante:

https://github.com/rgeminas/gp--/blob/master/src/scope/darray.h

#define DARRAY_DEFINE(name, type) DARRAY_TYPEDECL(name, type) DARRAY_IMPL(name, type)

// This is one single long line
#define DARRAY_TYPEDECL(name, type) \
typedef struct darray_##name \
{ \
    type* base; \
    size_t allocated_mem; \
    size_t length; \
} darray_##name; 

// This is also a single line
#define DARRAY_IMPL(name, type) \
static darray_##name* darray_init_##name() \
{ \
    darray_##name* arr = (darray_##name*) malloc(sizeof(darray_##name)); \
    arr->base = (type*) malloc(sizeof(type)); \
    arr->length = 0; \
    arr->allocated_mem = 1; \
    return arr; \
}

La macro DARRAY_TYPEDECL crée efficacement une définition de structure (sur une seule ligne), remplaçant name par le nom que vous passez et stockant un tableau des type que vous passez (le name est là pour que vous puissiez le concaténer au nom de la structure de base et avoir toujours un identifiant valide - darray_int * n'est pas un nom valide pour une structure), tandis que la macro DARRAY_IMPL définit les fonctions qui opèrent sur cette structure (dans ce cas, elles '' re marqué statique juste pour qu'on n'appelle la définition qu'une seule fois et non pas tout séparer).

Cela serait utilisé comme:

#include "darray.h"
// No types have been defined yet
DARRAY_DEFINE(int_ptr, int*)

// by this point, the type has been declared and its functions defined
darray_int_ptr* darray = darray_int_ptr_init();
1
Renan Gemignani

Je pense que les modèles sont beaucoup utilisés pour réutiliser des types de conteneurs qui ont beaucoup de valeurs algorithmiques telles que les tableaux dynamiques (vecteurs), les cartes, les arbres, etc. le tri, etc.

Sans modèles, nécessairement, ces implémentations contiennent sont écrites de manière générique et reçoivent juste assez d'informations sur le type requis pour leur domaine. Par exemple, avec un vecteur, ils ont juste besoin que les données soient blt'ables et ils ont besoin de connaître la taille de chaque élément.

Disons que vous avez une classe de conteneur appelée Vector qui fait cela. Cela prend vide *. L'utilisation simpliste de cela serait que le code de la couche d'application fasse beaucoup de transtypage. Donc, s'ils gèrent des objets Cat, ils doivent lancer Cat * pour annuler *, et revenir partout. La mise en litière de code d'application avec des transformations pose des problèmes évidents.

Les modèles résolvent cela.

Une autre façon de le résoudre consiste à créer un type de conteneur personnalisé pour le type que vous stockez dans le conteneur. Donc, si vous avez une classe Cat, vous créez une classe CatList dérivée de Vector. Vous surchargez ensuite les quelques méthodes que vous utilisez, en introduisant des versions qui prennent des objets Cat au lieu de void *. Donc, vous surchargeriez la méthode Vector :: Add (void *) avec Cat :: Add (Cat *), qui transmet simplement en interne le paramètre à Vector :: Add (). Ensuite, dans votre code d'application, vous appelleriez la version surchargée de Add lors du passage dans un objet Cat et évitez ainsi le transtypage. Pour être juste, la méthode Add ne nécessiterait pas de transtypage car un objet Cat * se convertit en void * sans transtypage. Mais la méthode pour récupérer un élément, comme la surcharge d'index ou une méthode Get () le ferait.

L'autre approche, dont je me souviens du seul exemple du début des années 90 avec un grand cadre d'application, est d'utiliser un utilitaire personnalisé qui crée ces types sur des classes. Je crois que MFC a fait cela. Ils avaient différentes classes pour les conteneurs comme CStringArray, CRectArray, CDoulbeArray, CIntArray, etc. Plutôt que de conserver le code en double, ils ont fait un certain type de méta-programmation similaire aux macros, en utilisant un outil externe qui générerait les classes. Ils ont rendu l'outil disponible avec Visual C++ au cas où quelqu'un voudrait l'utiliser - je ne l'ai jamais fait. Peut-être que j'aurais dû. Mais à l'époque, les experts vantaient, "Sous-ensemble sain de C++" et "Vous n'avez pas besoin de modèles"

1
zumalifeguard