Un peu de magie noire avec C++

Saturne dévorant un de ses enfants, de Goya

"Fig. 1 - Le C++"

Récemment confronté à des problèmes de programmation en C++ assez épineux en rapport avec les template, ces fameux types variables, je me suis juré d’écrire au moins un petit article dessus une fois que j’aurais fini. Voici les notes de mon retour des enfers…

Mais d’abord, il est bon de revenir à la question primordiale :

Qu’est-ce qu’un template ?

Wikipédia nous dit :

Les templates permettent d’écrire des fonctions et des classes en paramétrant le type de certains de leurs constituants (type des paramètres ou type de retour pour une fonction, type des éléments pour une classe collection par exemple).

Derrière des mots savants se cachent un concept plutôt simple, imaginé par un gros fainéant (sûrement un informaticien) : en C(++), de nombreuses opérations se font de la même façon, même pour des types différents. Concrètement, cela revient par exemple à dire : vérifier que deux entiers sont égaux, ce n’est pas si différent de vérifier que deux nombres à virgule sont égaux, ou que deux booléens sont égaux… Mieux: pour tous ces types (et de nombreux autres), cela marche exactement de la même manière. Du coup, plutôt que de réécrire une fonction pour chaque type faisant exactement la même chose à chaque fois, il est beaucoup plus commode de déclarer une seule et même fonction template, c’est-à-dire que contrairement à une fonction normale, elle ne connaît pas a priori le(s) type(s) qu’elle va manipuler, et c’est l’utilisateur qui au moment de l’appeler spécifie le type : « j’appelle la fonction machin pour qu’elle travaille avec des variables de type truc ».

Bon, pour la théorie, une pléthore de sites bien plus experts que moi existent, je ne vais pas non plus réexpliquer tout le principe, ce n’est pas mon but aujourd’hui. 🙂

Ceci étant dit, la suite de l’article concerne essentiellement les gens ayant une bonne connaissance des templates et des concepts du C++ en général.

« Une p’tite signature ici s’vous plaît »

Savez-vous qu’une fonction laisse une signature ? Il n’est pas ici question d’autographes mais bien de type du langage !

En gros, ce qu’on appelle la signature d’une fonction est le type représenté par le type de sa valeur de retour couplé aux types de ses paramètres.

Par exemple, une fonction int test(bool example1, float example2) — retournant et entier et prenant en paramètre un booléen et un nombre à virgule flottante — aura comme signature < int (bool, float) >.

Et bien, malgré les apparences, cette signature est bien un seul et même type.

Pour votre compilateur, < int (bool, float) > représente un unique type (et non pas trois comme on pourrait le penser) : quelque part pour lui, il s’agit du type d’une fonction retournant un entier et prenant en paramètre un booléen et un nombre à virgule.

Alors, quel est le rapport entre signature de fonction et template me direz-vous ?

La réponse se trouve dans un concept assez ancien de la programmation : le foncteur.

L’objet qui voulait être une fonction

Le foncteur, ou plus communément function object en anglais, est, comme ce dernier nom le laisse davantage sous-entendre que le premier, un objet se comportant comme une fonction, c’est-à-dire qu’on le dit «appelable» : ce qu’on entend par là, c’est que c’est un objet avec lequel on peut par exemple faire « objet(param1, param2) » comme nous le ferions pour une fonction f(param1, param2). L’article anglais de Wikipédia sur les function objects est d’ailleurs très complet à ce sujet.

Le problème, c’est qu’une application simple, basique du foncteur serait de déclarer une simple fonction (ou un pointeur sur fonction) à l’intérieur appelée lors de l’ « appel » de l’objet (en réalité on n’appelle pas l’objet, mais une fonction à l’intérieur : en C++ par exemple, on effectue cette « supercherie » en surchargeant l’opérateur () )…

Mais se pose le problème lorsqu’on cherche à créer un foncteur générique, auquel il suffirait de passer n’importe quel pointeur sur fonction (pouvant avoir n’importe quelle signature de fonction, vous suivez ?). En effet, comment créer un foncteur qui accepterait n’importe quelle fonction ? Les templates sont là pour ça !

Pourtant, d’autres problèmes demeurent (encore) … Comme nous l’avons vu précedemment la signature d’une fonction — son type de retour, le nombre et le type de ses paramètres — ne constitue qu’un seul et même type pour le compilateur. Comment alors gérer des fonctions prenant par exemple un nombre de paramètres différents ? Encore un autre problème : en C++, le foncteur utilise la surcharge de l’opérateur parenthèses ( « () » ) pour appeler la fonction qu’il contient. Cet appel se devant d’être équivalent à un appel de la fonction elle-même, il faut qu’il ait également le même type de retour. Mais, encore une fois, la signature de la fonction étant un seul et même paramètre, comment pouvons-nous « isoler » en quelque sorte le type de retour ?

Une solution est d’utiliser la puissante incantation appelée:  spécialisations de templates polymorphiques … 🙂

« À vos souhaits »

Le Cri

Étudiant devant le problème des templates polymorphiques

 

L’idée n’est pas très compliquée : la difficulté réside essentiellement dans la syntaxe particulièrement sévère de C++ (notamment en terme de templates). Mais sinon, le principe n’est pas beaucoup plus compliqué qu’une spécialisation de template classique (c’est-à-dire une fonction/classe template, mais pour laquelle on spécifie une utilisation précise dans certains cas particuliers).

Prenons une classe templatée classique :

1
2
template <typename T>
class Function;

Notez bien qu’on la déclare ne prenant qu’un seul paramètre de type template : en effet, dans le cas d’un foncteur ce qui nous intéresse c’est le faire varier en fonction de la signature de la fonction contenue. Signature qui ne représente qu’un seul et même type pour le compilateur. Donc nous n’avons besoin que de faire varier un seul type.

Ensuite, pour gérer des fonctions à arités multiples (pour utiliser des mots un peu savants), nous allons donc faire… des spécialisations !

En effet, la subtilité à comprendre ici est qu’en fait, on peut faire une spécialisation de template spécifiant moins de types que de paramètres template fournis.

Relisez la phrase. Encore une fois.

Je m’explique : une signature de fonction, c’est un seul et même type. Et pourtant, vu qu’il « englobe » le type de retour et ceux des paramètres de la fonction, il y a moyen de le « recoller » (pour avoir envie de faire ce genre de chose en C++, je pense que le champ lexical du bricolage est plutôt bien adapté 🙂 ) !

Ainsi, on peut faire une spécialisation sur une seule signature (exemple : un type de retour et 3 paramètres) en spécifiant 4 paramètres templates. Pour reprendre notre exemple, cela reviendrait à faire :

1
2
3
4
5
template <typename R, typename T1, typename T2, typename T3>
class Function<R (T1, T2, T3)>
{
...
};

Malgré les apparences, on a bien fait une spécialisation (le type spécialisé est celui entre chevrons) sur un seul type : le type « R (T1, T2, T3)« . Même si on a déclaré quatre types template.

À partir de là, il devient très facile de faire par exemple :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename R, typename T1, typename T2, typename T3>
class Function <R (T1, T2, T3)>
{
typedef R (*fptr)(T1, T2, T3);

protected:
fptr function_;

public:
R operator()(T1 p1, T2 p2, T3 p3)
{
return (*function_)(p1, p2, p3);
}
}

Et voilà on a un foncteur qui permet de gérer des fonctions avec trois arguments (et de retourner le bon type lors de l’appel à la fonction !). Et cela fonctionne pour n’importe quel nombre d’arguments : la seule limite de ce système est qu’il faille spécialiser la classe autant de fois que nous voulons gérer de nombres d’arguments différents.

 

Voilà, personnellement, j’ai eu à gérer un cas encore un peu plus compliqué, mais cet aperçu de code montre bien que les templates C++ sont à la fois un outil extrêmement puissant, compliqué, et bizarre à la fois, et qu’ils permettent parfois de faire des trucs assez surprenants. Ce cas d’utilisation est à ma connaissance le seul vrai moyen de faire des foncteurs génériques en C++ d’ailleurs. Cela va probablement changer avec l’arrivée des templates variadiques dans le nouveau standard C++11… Mais ce sera peut-être le sujet d’une autre histoire. 🙂

Un passage par cette adresse m’a été également très utile pour comprendre le principe (c’est un peu pour ça que je n’ai pas beaucoup détaillé mon exemple ; ceux qu’on peut trouver sur cette page sont complets et bien expliqués) de la spécialisation de types templates.

Enfin, après toutes ces infernales péripéties, moi, quelque part, je pose la question « Pourquoi faire compliqué en C++ quand on peut faire simple en Python ? » … 🙂

Le Python guidant le peuple

À gauche, deux adeptes de C++ encore dubitatifs

 

À bientôt 🙂

(Et bonnes fêtes de fin d’année !)

2 réflexions au sujet de « Un peu de magie noire avec C++ »

  1. malick

    Bonjour,
    Merci pour l’explication sur les foncteurs.
    J’avoue que même avec le max d’humour utilisé sur cette page, je figure sur le dernier tableau(je n’est toujours pas saisie l’utilité, le concept et l’utilisation de cet objet maléfique(le foncteur).

    Et encore merci

    Répondre
    1. Pando Auteur de l’article

      Hello,
      oh, concernant les foncteurs… Ça fait un moment que je ne m’en suis pas servi, mais de mon point de vue, ce sont un peu les Balrogs de la programmation: des trucs puissants, mais anciens et difficiles à manipuler, qu’on a fini par remplacer par des choses plus facilement contrôlables.
      J’en ai vu une application il y a un moment, en mathématiques: dans une situation où il fallait pouvoir appeler soit une fonction, soit une autre, les deux ne prenant pas les mêmes nombres/types de paramètres. C’est peut-être simple dans d’autres langages, mais en C++, il faut encore passer par un foncteur, et c’est encore une des manières les plus simples. Le vrai défi étant vraiment de faire un foncteur totalement générique (c’est une des rares applications où on voit la limite des templates C++ d’ailleurs).

      Une bonne journée et merci d’être passé 🙂

      Répondre

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.