65 votes

Métaprogrammation en C++ et en D

Le mécanisme des modèles en C++ n'est devenu utile qu'accidentellement pour la métaprogrammation des modèles. D'un autre côté, D's a été conçu spécifiquement pour faciliter cela. Et apparemment, il est encore plus facile à comprendre (c'est du moins ce que j'ai entendu dire).

Je n'ai pas d'expérience avec D, mais je suis curieux de savoir ce que l'on peut faire en D et pas en C++, en ce qui concerne la métaprogrammation de modèles.

8 votes

S'ils sont tous les deux turing complets, la réponse est rien :)

9 votes

@awoodland : Cela n'est vrai que pour une définition très limitée de "faire". Selon toute définition normale, il y a beaucoup de choses que vous ne pouvez pas faire avec les templates C++ (écrire dans des fichiers par exemple - mais j'imagine que vous ne pouvez pas non plus le faire avec la métaprogrammation de templates en D).

8 votes

@awoodland : Turing tarpit, quelqu'un ? ;)

69voto

Jonathan M Davis Points 19569

Les deux éléments les plus importants qui facilitent la métaprogrammation de modèles dans D sont les contraintes de modèles et static if - deux éléments que le C++ pourrait théoriquement ajouter et qui lui seraient très utiles.

Les contraintes de modèle vous permettent de mettre une condition sur un modèle qui doit être vraie pour que le modèle puisse être instancié. Par exemple, voici la signature de l'un des modèles suivants std.algorithm.find Les surcharges de l

R find(alias pred = "a == b", R, E)(R haystack, E needle)
    if (isInputRange!R &&
        is(typeof(binaryFun!pred(haystack.front, needle)) : bool))

Pour que cette fonction modèle puisse être instanciée, le type R doit être une plage d'entrée telle que définie par std.range.isInputRange (donc isInputRange!R doit être true ), et le prédicat donné doit être une fonction binaire qui se compile avec les arguments donnés et renvoie un type implicitement convertible en bool . Si le résultat de la condition dans la contrainte du modèle est false le modèle ne sera pas compilé. Non seulement cela vous protège des vilaines erreurs de gabarit que vous obtenez en C++ lorsque les gabarits ne se compilent pas avec leurs arguments donnés, mais cela vous permet de surcharger les gabarits en fonction de leurs contraintes de gabarit. Par exemple, il existe une autre surcharge de find qui est

R1 find(alias pred = "a == b", R1, R2)(R1 haystack, R2 needle)
if (isForwardRange!R1 && isForwardRange!R2
        && is(typeof(binaryFun!pred(haystack.front, needle.front)) : bool)
        && !isRandomAccessRange!R1)

Il prend exactement les mêmes arguments, mais sa contrainte est différente. Ainsi, différents types fonctionnent avec différentes surcharges de la même fonction modèle, et la meilleure implémentation de la fonction find peuvent être utilisés pour chaque type. Il n'y a aucun moyen de faire ce genre de choses proprement en C++. Avec un peu de familiarité avec les fonctions et les modèles utilisés dans une contrainte de modèle typique, les contraintes de modèle en D sont assez faciles à lire, alors qu'il faut une métaprogrammation de modèle très compliquée en C++ pour même tenter quelque chose comme ça, que le programmeur moyen ne sera pas capable de comprendre, et encore moins de faire tout seul. Boost en est un excellent exemple. Il fait des choses incroyables, mais il est incroyablement compliqué.

static if améliore encore la situation. Comme pour les contraintes de modèle, toute condition pouvant être évaluée au moment de la compilation peut être utilisée avec cette contrainte.

static if(isIntegral!T)
{
    //...
}
else static if(isFloatingPoint!T)
{
    //...
}
else static if(isSomeString!T)
{
    //...
}
else static if(isDynamicArray!T)
{
    //...
}
else
{
    //...
}

La branche qui est compilée dépend de la condition qui est évaluée en premier lieu à true . Ainsi, à l'intérieur d'un modèle, vous pouvez spécialiser des parties de son implémentation en fonction des types avec lesquels le modèle a été instancié - ou en fonction de tout autre élément qui peut être évalué au moment de la compilation. Par exemple, core.time utilise

static if(is(typeof(clock_gettime)))

de compiler le code différemment selon que le système fournit ou non des clock_gettime ou non (si clock_gettime est présent, il l'utilise, sinon il utilise gettimeofday ).

L'exemple le plus frappant que j'ai vu où D améliore les modèles est probablement celui d'un problème que mon équipe a rencontré en C++. Nous devions instancier un modèle différemment selon que le type qui lui était donné était dérivé d'une classe de base particulière ou non. Nous avons fini par utiliser une solution basée sur cette question de stack overflow . Cela fonctionne, mais c'est assez compliqué pour tester si un type est dérivé d'un autre.

En revanche, dans D, il suffit d'utiliser la fonction : opérateur.

auto func(T : U)(T val) {...}

Si T est implicitement convertible en U (comme ce serait le cas si T ont été obtenues à partir de U ), alors func sera compilé, alors que si T n'est pas implicitement convertible en U alors il ne le fera pas. Cela une simple amélioration rend les spécialisations de modèles, même les plus basiques, beaucoup plus puissantes (même sans contraintes de modèles ou de static if ).

Personnellement, j'utilise rarement les modèles en C++, sauf pour les conteneurs et les fonctions occasionnelles en <algorithm> Il n'est pas possible de les utiliser, car ils sont trop pénibles à mettre en œuvre. Ils donnent lieu à de vilaines erreurs et il est très difficile de faire quelque chose de fantaisiste avec. Pour faire quoi que ce soit d'un peu compliqué, il faut être très doué avec les modèles et la métaprogrammation des modèles. Avec les modèles en D, c'est tellement facile que je les utilise tout le temps. Les erreurs sont beaucoup plus faciles à comprendre et à gérer (bien qu'elles soient toujours pires que les erreurs typiques des fonctions non modélisées), et je n'ai pas besoin de comprendre comment forcer le langage à faire ce que je veux avec une métaprogrammation fantaisiste.

Il n'y a aucune raison pour que le C++ ne puisse pas bénéficier d'une grande partie des capacités de D (les concepts C++ seraient utiles s'ils étaient un jour mis au point), mais jusqu'à ce qu'ils ajoutent une compilation conditionnelle de base avec des constructions similaires aux contraintes de modèles et à l'approche static if en C++, les modèles C++ ne pourront pas être comparés aux modèles D en termes de facilité d'utilisation et de puissance.

0 votes

Vous pouvez déclarer des variables de différents types dans static if . Extrêmement utile.

0 votes

Le C++ dispose désormais de contraintes de gabarit et d'une variante de static if

40voto

bitmask Points 11086

Je pense que rien n'est mieux placé pour démontrer l'incroyable puissance (TM) du système de gabarit D que ce moteur de rendu Je l'ai trouvé il y a des années :

The compiler output

Oui ! C'est en fait ce qui est généré par la fonction compilateur ... c'est le "programme", et il est très coloré.

Editer

La source semble être de nouveau en ligne.

0 votes

Cool ! Une idée de l'endroit où trouver la source ?

0 votes

Je n'arrive pas à le retrouver (je crois que je l'ai téléchargé il y a un certain temps). Mais même si je le trouvais sur l'un de mes disques, je ne suis pas sûr qu'il serait légal de le partager. Peut-être pourrait-on demander à l'auteur de corriger le lien (il est le plus n'a probablement pas été brisée intentionnellement).

1 votes

Par ailleurs, le code source a été écrit il y a plusieurs années (comme le mentionne la page d'accompagnement) - une grande partie du code qui s'y trouve (en particulier le code dans le répertoire meta/) peut être largement simplifié et raccourci en raison des changements apportés à D, même sans s'approcher de l'exécution de la fonction au moment de la compilation.

27voto

dsimcha Points 32831

Les meilleurs exemples de métaprogrammation D sont les modules de la bibliothèque standard D qui en font un usage intensif par rapport aux modules Boost et STL du C++. Consultez le site de D std.range , std.algorithme , std.fonctionnel y std.parallelisme . Aucun de ces éléments ne serait facile à mettre en œuvre en C++, du moins avec le type d'API propre et expressive dont disposent les modules D.

La meilleure façon d'apprendre la métaprogrammation D, selon moi, est de s'appuyer sur ce type d'exemples. J'ai appris en grande partie en lisant le code de std.algorithm et de std.range, qui ont été écrits par Andrei Alexandrescu (un gourou de la métaprogrammation de modèles C++ qui s'est fortement impliqué dans D). J'ai ensuite utilisé ce que j'avais appris et contribué au module std.parallelism.

Notez également que D dispose d'une évaluation des fonctions au moment de la compilation (CTFE) qui est similaire à celle de C++1x. constexpr mais beaucoup plus général en ce sens qu'un sous-ensemble important et croissant de fonctions qui peuvent être évaluées au moment de l'exécution peuvent être évaluées sans modification au moment de la compilation. Ceci est utile pour la génération de code au moment de la compilation, et le code généré peut être compilé à l'aide de la fonction mixins de chaînes de caractères .

1 votes

Pour le CFTE, vous pouvez lire mon article de blog pour une explication plus complète : giovanni.bajo.it/2010/05/compile-time-function-execution-in-d

14voto

Trass3r Points 1505

En D, il est facile d'imposer la statique contraintes sur les paramètres du modèle et écrire du code en fonction de l'argument du modèle avec statique si .
Il est possible de simuler cela pour des cas simples avec le C++ en utilisant la spécialisation des modèles et d'autres astuces (voir boost) mais c'est une PITA et très limité car le compilateur n'expose pas beaucoup de détails sur les types.

Une chose que le C++ ne peut vraiment pas faire, c'est la génération sophistiquée de code au moment de la compilation.

11voto

Mehrdad Points 70493

Voici un morceau de code D qui exécute une opération de map() qui renvoie ses résultats par référence .

Il crée deux tableaux de longueur 4, cartes chaque paire d'éléments correspondante à l'élément ayant la valeur minimale, et la multiplie par 50, et stocke le résultat dans le tableau d'origine .

Les caractéristiques importantes à noter sont les suivantes :

  • Les modèles sont variés : map() peut prendre n'importe quel nombre d'arguments.

  • Le code est (relativement) courte ! Les Mapper qui est la logique de base, ne fait que 15 lignes - et pourtant elle peut faire tant avec si peu. Ce que je veux dire, ce n'est pas que c'est impossible en C++, mais que ce n'est certainement pas aussi compact et propre.


import std.metastrings, std.typetuple, std.range, std.stdio;

void main() {
    auto arr1 = [1, 10, 5, 6], arr2 = [3, 9, 80, 4];

    foreach (ref m; map!min(arr1, arr2)[1 .. 3])
        m *= 50;

    writeln(arr1, arr2); // Voila! You get:  [1, 10, 250, 6][3, 450, 80, 4]
}

auto ref min(T...)(ref T values) {
    auto p = &values[0];
    foreach (i, v; values)
        if (v < *p)
            p = &values[i];
    return *p;
}

Mapper!(F, T) map(alias F, T...)(T args) { return Mapper!(F, T)(args); }

struct Mapper(alias F, T...) {
    T src;  // It's a tuple!

    @property bool empty() { return src[0].empty; }

    @property auto ref front() {
        immutable sources = FormatIota!(q{src[%s].front}, T.length);
        return mixin(Format!(q{F(%s)}, sources));
    }

    void popFront() { foreach (i, x; src) { src[i].popFront(); } }

    auto opSlice(size_t a, size_t b) {
        immutable sliced = FormatIota!(q{src[%s][a .. b]}, T.length);
        return mixin(Format!(q{map!F(%s)}, sliced));
    }
}

// All this does is go through the numbers [0, len),
// and return string 'f' formatted with each integer, all joined with commas
template FormatIota(string f, int len, int i = 0) {
    static if (i + 1 < len)
        enum FormatIota = Format!(f, i) ~ ", " ~ FormatIota!(f, len, i + 1);
    else
        enum FormatIota = Format!(f, i);
}

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