web-dev-qa-db-fra.com

MetaProgramming en C ++ et en D

Le mécanisme de modèle en C++ ne s'est accidentellement utile que pour les métaprogrammations de modèles. D'autre part, D a été conçu spécifiquement pour faciliter cela. Et apparemment, il est encore plus facile de comprendre (ou donc j'ai entendu).

Je n'ai aucune expérience avec d, mais je suis curieux, qu'est-ce que vous pouvez faire en D et que vous ne pouvez pas en C++, en ce qui concerne le modèle de métaprogrammation?

65
Paul Manta

Les deux plus grandes choses qui aident le modèle métaprogramming en D sont des contraintes de modèle et static if - Les deux, tous deux C++ pourraient ajouter théoriquement et ce qui le profiterait grandement.

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

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élise puisse être instanciée, le type R doit être une plage d'entrée tel que définie par std.range.isInputRange (donc isInputRange!R Doit être true), et le prédicat donné doit être une fonction binaire qui compile avec les arguments donnés et renvoie un type implicitement convertible à bool. Si le résultat de la condition dans la contrainte de modèle est false, alors le modèle ne compilera pas. Non seulement cela vous protège des erreurs de modèles méchantes que vous obtenez en C++ lorsque les modèles ne compilent pas avec leurs arguments donnés, mais cela le rend afin que vous puissiez surcharger des modèles en fonction de leurs contraintes de modèle. Par exemple, il y a 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 faut exactement les mêmes arguments, mais sa contrainte est différente. Donc, différents types fonctionnent avec différentes surcharges de la même fonction modèles et la meilleure implémentation de find peut être utilisée pour chaque type. Il n'y a aucun moyen de faire ce genre de chose proprement en C++. Avec un peu de familiarité avec les fonctions et les modèles utilisés dans votre contrainte de modèle typique, les contraintes de modèle en D sont assez faciles à lire, alors que vous avez besoin d'un métaprogrammation de modèles très compliqués dans C++ pour tenter de tenter quelque chose comme ceci, que votre programmateur moyen n'est pas Aller pour pouvoir comprendre, encore moins faire pour eux-mêmes. Boost est un excellent exemple de ceci. Cela fait des choses étonnantes, mais c'est incroyablement compliqué.

static if améliore encore la situation. Tout comme avec des contraintes de modèle, toute condition pouvant être évaluée au moment de la compilation peut être utilisée avec elle. par exemple.

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

Quelle branche est compilée dans dépend de quelle condition évalue d'abord à true. Donc, dans un modèle, vous pouvez spécialiser des morceaux de sa mise en œuvre sur la base des types que le modèle a été instancié avec - ou sur la base de toute autre chose qui peut être évaluée à la compilation. Par exemple, core.time les usages

static if(is(typeof(clock_gettime)))

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

Probablement l'exemple le plus purs que j'ai vu où D amélioration sur les modèles est avec un problème que mon équipe au travail s'est déroulé en C++. Nous avions besoin d'instancier un modèle différemment sur la base de savoir si le type qu'il a été 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 dépassement de pile . Cela fonctionne, mais il est assez compliqué de vérifier simplement si un type est dérivé d'un autre.

Dans D, toutefois, tout ce que vous avez à faire est d'utiliser le : Opérateur. par exemple.

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

Si T est implicitement convertible à U (comme il serait si T a été dérivé de U), puis func _ _ va compilera, Alors que si T _ n'est pas implicitement convertible à U, alors ce n'est pas le cas. que une amélioration simple rend les spécialisations de modèles de base beaucoup plus puissantes (même sans contraintes de modèle ni static if).

Personnellement, j'utilise rarement des modèles en C++ autres qu'avec des conteneurs et la fonction occasionnelle dans <algorithm>, parce qu'ils sont tellement douloureux à utiliser. Ils aboutissent à des erreurs laides et sont très difficiles à faire quelque chose de fantaisie. Pour faire quelque chose même un peu compliqué, vous devez être très habile avec des modèles et des modèles métaprogrammants. Avec des modèles en D, il est si facile que je les utilise tout le temps. Les erreurs sont beaucoup plus faciles à comprendre et à traiter (bien qu'elles soient encore pires que d'erreurs sont généralement avec des fonctions non modèles), et je n'ai pas à comprendre comment forcer la langue à faire ce que je veux avec des métaprogrammations de fantaisie .

Il n'y a aucune raison que c ++ n'a pas pu gagner beaucoup de ces capacités que D (concepts C++ contribuerait-il s'ils sont toujours ceux qui sont sortis), mais jusqu'à ce qu'ils ajoutent une compilation conditionnelle de base avec des constructions similaires aux contraintes de modèle et static if à C++, les modèles C++ ne seront tout simplement pas en mesure de comparer avec des modèles D en termes de facilité d'utilisation et de puissance.

68
Jonathan M Davis

Je crois que rien n'est mieux qualifié pour montrer l'incroyable puissance (TM) du système de modèle D que ce rend J'ai trouvé il y a des années:

The compiler output

Oui! C'est en fait ce qui est généré par le compilateur ... c'est le "programme", et assez coloré, en effet.

Éditer

La source semble être de retour en ligne.

40
bitmask

Les meilleurs exemples de Métaprogramming D sont des modules de bibliothèque standard qui utilisent de lourdes utilisations sur les modules Boost et STL de C++. Vérifiez D STD.Range , STD.ALGORITHM , STD.FONCTIONNEL et STD.PARALLESMISMER . Aucun de ceux-ci ne serait facile à mettre en œuvre en C++, au moins avec le type d'API propre et expressif que les modules D ont.

La meilleure façon d'apprendre d métaprogramming, IMHO, est par ce type d'exemples. J'ai appris en grande partie en lisant le code dans STD.ALGORITHM et STD.RANGE, qui ont été écrits par Andrei Alexandrescu (un modèle C++ MetaProgramming Guru qui est devenu lourdement impliqué avec d). J'ai ensuite utilisé ce que j'ai appris et a contribué au module STD.ParLeLism.

Notez également que D compile une évaluation de la fonction d'heure (CTFE) qui est similaire à C++ 1x constexpr mais beaucoup plus général en ce qu'un sous-ensemble grand et croissant de fonctions pouvant être évaluées au moment de l'exécution peut être évaluée non modifiée à compiler l'heure. Ceci est utile pour la génération de code temporel de compilation et le code généré peut être compilé à l'aide de Mélangements de chaîne .

27
dsimcha

Bien en D, vous pouvez facilement imposer des statiques contraintes sur les paramètres de modèle et code d'écriture en fonction de l'argument de modèle réel avec statique si .
[.____] Il est possible de simuler que pour des cas simples avec C++ en utilisant une spécialisation de modèle et d'autres astuces (voir Boost), mais c'est un pita et une cause très limitée que le compilateur n'expose pas de nombreux détails sur les types.

Une chose C++ ne peut vraiment pas faire est une génération de code temporelle de compilation sophistiquée.

14
Trass3r

Voici un morceau de code D qui fait une fonction de fabrication sur mesure map() qui renvoie ses résultats par référence .

Il crée deux matrices de longueur 4, Maps Chaque paire d'éléments correspondante à l'élément avec la valeur minimale et la multiplie de 50, et stocke le résultat dans le tableau d'origine .

Certaines fonctionnalités importantes à noter sont les suivantes:

  • Les modèles sont variadiques: map() Pourrait prendre un nombre quelconque d'arguments.

  • Le code est (relativement) court! La structure Mapper, qui est la logique principale, n'est que de 15 lignes - et pourtant cela peut faire tellement de choses avec si peu. Mon point n'est pas que cela soit impossible en C++, mais cela 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);
}
11
Mehrdad

J'ai écrit mes expériences avec des modèles de S, des mélanges à cordes et des mixines de modèle: http://david.rothlis.net/d/templates/

Cela devrait vous donner une saveur de ce qui est possible dans D - Je ne pense pas qu'en C++, vous pouvez accéder à un identifiant en tant que chaîne, transformer cette chaîne à la compilation et générer du code de la chaîne manipulée.

Ma conclusion: extrêmement flexible, extrêmement puissante et utilisable par de simples mortels, mais le compilateur de référence est encore un peu buggy lorsqu'il s'agit des trucs de métaprogramming de compilation plus avancés.

9

Manipulation de chaîne, même analyse de chaîne.

Ceci est un MP qui génère des analyseurs décents récursifs basés sur des grammaires définies dans des chaînes utilisant (plus ou moins) BNF. Je ne l'ai pas touché depuis des années, mais ça travaillait.

8
BCS

en D, vous pouvez vérifier la taille d'un type et des méthodes disponibles dessus et de choisir la mise en œuvre que vous souhaitez utiliser.

ceci est utilisé par exemple dans le core.atomic module

bool cas(T,V1,V2)( shared(T)* here, const V1 ifThis, const V2 writeThis ){
    static if(T.sizeof == byte.sizeof){
       //do 1 byte CaS
    }else static if(T.sizeof == short.sizeof){
       //do 2 byte CaS
    }else static if( T.sizeof == int.sizeof ){
       //do 4 byte CaS
    }else static if( T.sizeof == long.sizeof ){
       //do 8 byte CaS
    }else static assert(false);
}
6
ratchet freak

Juste pour contrer le post de traçage D, voici une compilation C++ Time Ray Tracer ( MetaTrace ):

enter image description here

(Au fait, il utilise principalement des métaprogramming C++ 2003; il serait plus lisible avec le nouveau constexprs)

4
Sebastian Mach

Il est silencieux quelques éléments que vous pouvez faire dans des modèles métaprogrammants dans D que vous ne pouvez pas faire en C++. La chose la plus importante est que vous puissiez faire des modèles de métaprogrammation sans SO une grande douleur!

1
Ralph Tandetzky