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

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

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

Ответ 1

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

Ограничения шаблона позволяют вам установить условие для шаблона, которое должно быть истинным для того, чтобы шаблон мог быть создан. Например, это сигнатура одной из перегрузок 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))

Чтобы эта templated-функция могла быть создана, тип R должен быть диапазоном ввода, как определено std.range.isInputRange (поэтому isInputRange!R должно быть true), и данному предикату необходимо быть двоичной функцией, которая компилируется с данными аргументами и возвращает тип, который неявно конвертируется в bool. Если результатом условия в шаблоне является false, то шаблон не будет компилироваться. Это не только защитит вас от неприятных ошибок шаблона, которые вы получаете на С++, когда шаблоны не будут компилироваться с их заданными аргументами, но это делает так, что вы можете перегружать шаблоны на основе ограничений их шаблонов. Например, существует еще одна перегрузка 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 может использоваться для каждого типа. В С++ нет никакого способа сделать что-то чисто. С небольшим знакомством с функциями и шаблонами, используемыми в вашем типичном ограничении шаблона, ограничения шаблона в D довольно легко читаются, тогда как вам нужно очень сложное метапрограммирование шаблонов на С++, чтобы даже попытаться что-то вроде этого, что ваш средний программист не чтобы быть в состоянии понять, не говоря уже о том, чтобы делать сами по себе. 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 улучшает шаблоны, - это проблема, с которой моя команда на работе столкнулась с С++. Нам нужно было создать экземпляр шаблона по-разному в зависимости от того, был ли тот тип, который он был указан, был получен из определенного базового класса или нет. Мы закончили использование решения на основе этого вопроса о переполнении стека. Он работает, но он довольно сложный для просто проверки того, является ли один тип производным от другого.

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

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

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

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

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

Ответ 2

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

The compiler output

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

Изменить

Кажется, что источник вернулся в сеть.

Ответ 3

Лучшие примеры метапрограммирования D - это стандартные библиотечные модули D, которые сильно используют его против модулей С++ Boost и STL. Проверьте D std.range, std.algorithm, std.functional и std.parallelism. Ни одно из них не было бы легко реализовать на С++, по крайней мере, с помощью чистого, выразительного API, который имеют D-модули.

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

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

Ответ 4

Хорошо в D вы можете легко наложить статические ограничения на параметры шаблона и написать код в зависимости от фактического аргумента шаблона с static, если.
Можно моделировать это для простых случаев с С++, используя специализированную специализацию и другие трюки (см. Boost), но это PITA и очень ограниченная причина, по которой компилятор не предоставляет много подробностей о типах.

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

Ответ 5

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

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

Ниже перечислены некоторые важные функции:

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

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


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

Ответ 6

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

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

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

Ответ 7

Обработка строк, даже синтаксический анализ строк.

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

Ответ 8

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

Ответ 9

Просто, чтобы противостоять сообщению трассировки D-лучей, здесь показан трассировщик лучей времени С++ (metatrace):

enter image description here

(кстати, он использует в основном метапрограммирование С++ 2003, он будет более читабельным с помощью нового constexpr s)

Ответ 10

Есть несколько вещей, которые вы можете сделать в метапрограммировании шаблонов в D, которые вы не можете сделать на С++. Самое главное, что вы можете делать метапрограммирование шаблонов БЕЗ ТАК БОЛЬШОЙ БОЛИ!