Механизм шаблонов в C++ только случайно стал полезен для метапрограммирования шаблонов. С другой стороны, D's был разработан специально для облегчения этой задачи. И, по-видимому, его даже легче понять (или я так слышал).
Я'не имею опыта работы с D, но мне'интересно, что именно можно делать в D и нельзя в C++, когда речь идет о метапрограммировании шаблонов?
Две самые большие вещи, которые помогают метапрограммированию шаблонов в 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++. Нам нужно было инстанцировать шаблон по-разному в зависимости от того, является ли тип, который ему был задан, производным от определенного базового класса или нет. В итоге мы использовали решение, основанное на этом вопросе stack overflow. Оно работает, но довольно сложно для простой проверки того, является ли один тип производным от другого.
В D, однако, все, что вам нужно сделать, это использовать оператор :
. Например.
auto func(T : U)(T val) {...}
Если T
неявно преобразуется в U
(как было бы, если бы T
был производным от U
), то func
будет компилироваться, тогда как если T
неявно преобразуется в U
, то не будет. Это простое улучшение делает даже базовые специализации шаблонов гораздо более мощными (даже без ограничений шаблонов или static if
).
Лично я редко использую шаблоны в C++, кроме контейнеров и случайных функций в <алгоритме>
, потому что их использование доставляет много хлопот. Они приводят к уродливым ошибкам, и с ними очень трудно сделать что-то причудливое. Чтобы сделать что-то хоть немного сложное, нужно быть очень опытным в использовании шаблонов и метапрограммировании шаблонов. Однако с шаблонами в D все настолько просто, что я использую их постоянно. Ошибки гораздо легче понять и разобраться с ними (хотя они все еще хуже, чем ошибки, обычно возникающие при использовании нешаблонных функций), и мне не нужно придумывать, как заставить язык делать то, что я хочу, с помощью причудливого метапрограммирования.
Нет причин, по которым C++ не мог бы получить большую часть этих возможностей, которые есть у D (концепции C++ помогут, если они когда-нибудь разберутся с этим), но пока они не добавят в C++ базовую условную компиляцию с конструкциями, подобными ограничениям шаблона и static if
, шаблоны C++ просто не смогут сравниться с шаблонами D по простоте использования и мощности.
Я считаю, что ничто не может лучше показать невероятную мощь (TM) системы шаблонов D, чем этот рендерер, который я нашел несколько лет назад:
Да! Это действительно то, что генерируется компилятором ... это "программа", и довольно красочная, на самом деле.
Похоже, что исходники снова доступны.
Лучшими примерами метапрограммирования в D являются модули стандартной библиотеки D, которые активно используют его в сравнении с модулями Boost и STL в C++. Посмотрите на D's std.range, std.algorithm, std.functional и std.parallelism. Ни одну из этих функций не было бы легко реализовать на C++, по крайней мере, с таким чистым и выразительным API, как у модулей D.
Лучший способ изучить метапрограммирование на D, IMHO, - это примеры такого рода. Я в основном учился, читая код std.algorithm и std.range, которые были написаны Андреем Александреску (гуру метапрограммирования шаблонов в C++, который сильно увлекся D). Затем я использовал полученные знания и внес свой вклад в модуль std.parallelism.
Также обратите внимание, что в D есть оценка функций во время компиляции (CTFE), которая похожа на constexpr
в C++1x', но гораздо более общая, поскольку большое и растущее подмножество функций, которые могут быть оценены во время выполнения, могут быть оценены без изменений во время компиляции. Это полезно для генерации кода во время компиляции, а сгенерированный код может быть скомпилирован с помощью string mixins.
В D вы можете легко наложить статические ограничения на параметры шаблона и писать код в зависимости от фактического аргумента шаблона с помощью static 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, которая генерирует рекурсивные приличные парсеры на основе грамматик, определенных в строках с использованием (более или менее) БНФ. Я не обращался к ней уже несколько лет, но раньше она работала.
в 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);
}
Для того, чтобы противостоять Д трассировки лучей поста, вот на C++ время компиляции трассировщика лучей (metatrace):
(кстати, она используется в основном на C++метапрограммирование 2003; было бы более читаемо с новых таких же)
Есть тихие несколько вещей, которые вы можете сделать в шаблон метапрограммирование в D, что нельзя сделать в C++. Самое главное заключается в том, что вы можете сделать шаблон метапрограммирование не столько боли!