Метапрограммирование в C++ и D


Шаблонный механизм в C++ только случайно оказался полезным для метапрограммирования шаблонов. С другой стороны, D был разработан специально, чтобы облегчить это. И, по-видимому, это даже легче понять (или так я слышал).

У меня нет опыта работы С D, но мне интересно, что вы можете сделать в D и не можете в C++, когда речь заходит о метапрограммировании шаблонов?

10 64

10 ответов:

Две самые большие вещи, которые помогают шаблонному метапрограммированию В D, - это ограничения шаблона и static if - оба из которых c++ теоретически может добавить и которые принесут ему большую пользу.

Ограничения шаблона позволяют поставить условие для шаблона, которое должно быть истинным, чтобы шаблон мог быть создан. Например, это сигнатура одной из перегрузок std.algorithm.find:

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

Для того, чтобы эта шаблонная функция могла быть создана, тип R должен быть входной диапазон, определенный std.range.isInputRange (поэтому isInputRange!R должен быть true), а данный предикат должен быть двоичной функцией, которая компилируется с заданными аргументами и возвращает тип, который неявно преобразуется в bool. Если результатом условия в ограничении шаблона является false, то шаблон не будет компилироваться. Это не только защищает вас от неприятных ошибок шаблонов, которые вы получаете в C++, когда шаблоны не будут компилироваться с заданными аргументами, но и делает это так, что вы можете перегружайте шаблоны, основываясь на их шаблонных ограничениях. Например, есть еще одна перегрузка find, которая является

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)
Он принимает точно такие же аргументы, но его ограничение отличается. Таким образом, разные типы работают с разными перегрузками одной и той же шаблонной функции, и для каждого типа можно использовать наилучшую реализацию find. В C++нет способа сделать такие вещи чисто. С небольшим знакомством с функциями и шаблонами, используемыми в вашем типичном шаблоне ограничения, ограничения шаблона в D довольно легко читаются, в то время как вам нужно очень сложное метапрограммирование шаблона в C++, чтобы даже попытаться сделать что-то подобное, что ваш средний программист не сможет понять, не говоря уже о том, чтобы сделать это самостоятельно. Boost является ярким примером этого. Он делает некоторые удивительные вещи, но это невероятно сложно.

static if это еще больше улучшает ситуацию. Так же, как и с шаблонными ограничениями, любое условие, которое может быть оцененный во время компиляции может быть использован с ним. например

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

Какая ветвь компилируется, зависит от того, какое условие сначала вычисляется как true. Таким образом, в шаблоне можно специализировать фрагменты его реализации на основе типов, с которыми был создан шаблон, или на основе чего - либо еще, что может быть оценено во время компиляции. Например, core.time использует

static if(is(typeof(clock_gettime)))

Для компиляции кода по-разному в зависимости от того, предоставляет ли система clock_gettime или нет (если clock_gettime существует, он использует его, иначе он использует gettimeofday).

Вероятно, самый яркий пример, который я видел, когда D улучшает шаблоны, - это проблема, с которой столкнулась моя команда на работе в C++. Нам нужно было создать экземпляр шаблона по-разному в зависимости от того, является ли данный тип производным от конкретного базового класса или нет. В итоге мы использовали решение, основанное наэтом вопросе переполнения стека . Это работает, но это довольно сложно для простого тестирования, является ли один тип производным от другой.

В D, однако, все, что вам нужно сделать, это использовать оператор :. например

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

Если T неявно преобразуется в U (как это было бы, если бы T были получены из U), то func будет компилироваться, тогда как если T не является неявно преобразуемым в U, то это не будет. что простое улучшение делает даже базовые шаблонные специализации намного более мощными (даже без ограничений шаблона или static if).

Лично я редко использую шаблоны в C++, кроме контейнеров и случайной функции в <algorithm>, потому что они очень болезненны в использовании. Они приводят к уродливым ошибкам и очень трудно сделать что-нибудь необычное. Чтобы сделать что-нибудь даже немного сложное, вам нужно быть очень опытным с шаблонами и шаблонным метапрограммированием. С шаблонами в D, однако, это так просто, что я использую их все время. Ошибки гораздо легче понять и справиться с ними (хотя они все еще хуже, чем ошибки, с которыми обычно сталкиваются не шаблонные функции), и мне не нужно выяснять, как заставить язык делать то, что я хочу, с помощью причудливого метапрограммирования.

Нет никаких причин, по которым C++ не мог бы получить большую часть этих способностей, которыми обладает D (концепции C++ помогут, если они когда-нибудь разберутся с ними), но до тех пор, пока они не добавят базовую условную компиляцию с конструкциями, подобными ограничениям шаблона и static if В C++, шаблоны C++ просто не смогут сравниться с шаблонами D с точки зрения простоты использования и удобства использования. сила.

Я считаю, что нет ничего лучше, чтобы показать невероятную силу (TM) системы шаблонов D, чем Этот визуализатор, который я нашел много лет назад:

Выходные данные компилятора

Да! Это фактически то, что генерируется компилятором ... это" программа", причем довольно красочная.

Edit

Источник, кажется, снова в сети.

Лучшими примерами D-метапрограммирования являются модули библиотеки D standard, которые интенсивно используют it по сравнению с модулями C++ Boost и STL. Проверьте d'S std.диапазон, ЗППП.алгоритм, ЗППП.функциональные и СТД.параллелизм . Ни один из них не был бы легко реализован в C++, по крайней мере, с таким чистым, выразительным API, который есть у модулей D.

Лучший способ научиться D-метапрограммированию, ИМХО, заключается в таких примерах. Я учился в основном, читая код для ЗППП.алгоритм и std.range, которые были написаны Андреем Александреску (гуру метапрограммирования шаблонов C++, который стал активно заниматься D). Затем я использовал то, что узнал, и внес свой вклад в ЗППП.модуль параллелизма.

Также обратите внимание, что у D есть функция оценки времени компиляции (CTFE), которая похожа на C++1x constexpr, но гораздо более общая в том, что большое и растущее подмножество функций, которые могут быть оценены во время выполнения, могут быть оценены немодифицированными во время компиляции. Это полезно для генерации кода во время компиляции, и сгенерированный код может быть скомпилирован с помощью string mixins .

Ну в D вы можете легко наложить статическиеограничения на параметры шаблона и написать код в зависимости от фактического аргумента шаблона сстатическим if .
Это можно смоделировать для простых случаев с C++ с помощью специализации шаблона и других трюков (см. boost), но это Пита и очень ограниченная причина, по которой компилятор не раскрывает много деталей о типах.

Одна вещь, которую C++ действительно просто не может сделать, - это сложная генерация кода во время компиляции.

Вот фрагмент кода D, который делает заказ map(), которыйвозвращает свои результаты по ссылке .

Он создает два массива длины 4, сопоставляет каждую соответствующую пару элементов элементу с минимальным значением и умножает его на 50, а сохраняет результат обратно в исходный массив.

Следует отметить следующие важные особенности:

  • Шаблоны являются вариадическими: map() может принимать любое число аргументы.

  • Код является (относительно) коротким ! Структура Mapper, которая является основной логикой, состоит всего из 15 строк-и все же она может сделать так много с таким малым. Я не говорю, что это невозможно в C++, но это, конечно, не так компактно и чисто.


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);
}

Я описал свой опыт работы с шаблонами D, строковыми миксинами и шаблонными миксинами: http://david.rothlis.net/d/templates/

Это должно дать вам представление о том, что возможно в D-Я не думаю, что в C++ вы можете получить доступ к идентификатору в виде строки, преобразовать эту строку во время компиляции и сгенерировать код из манипулируемой строки.

Мой вывод: чрезвычайно гибкий, чрезвычайно мощный и пригодный для использования простыми смертными, но компилятор ссылок все еще несколько багги, когда речь заходит о более продвинутом метапрограммировании во время компиляции.

Манипуляции со строками, даже синтаксический анализ строк.

Это библиотека MP , которая генерирует рекурсивные приличные Парсеры на основе грамматик, определенных в строках с использованием (более или менее) BNF. Я не прикасался к нему много лет, но раньше он работал.

В D вы можете проверить размер типа и доступные методы на нем и решить, какую реализацию вы хотите использовать

Это используется, например, в core.atomic модуль

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);
}

Просто для того, чтобы противостоять посту трассировки D-лучей, вот c++ compile time ray tracer (metatrace):

Введите описание изображения здесь

(Кстати, он использует в основном метапрограммирование C++2003; Это было бы более читаемым с новым constexprs)

Есть несколько вещей, которые вы можете сделать в шаблоне метапрограммирования в D, которые вы не можете сделать в C++. Самое главное, что вы можете делать шаблонное метапрограммирование без такой боли!