Доброго времени суток, хабр!
Сегодня поговорим о том, что делает метапрограммирование в D таким гибким и мощным — compile-time рефлексии. D позволяет программисту напрямую пользоваться информацией, которой оперирует компилятор, а не выводить её хитрыми способами. Так какую информацию позволяет получить компилятор и как её можно использовать?
Начнём с, наверное, самых частых в использовании, приёмов — выяснение валидности выражения:
И __traits(compiles, expr) и is(typeof(expr)) ждут валидное, с точки зрения лексики, выражение expr (например, выражение 12thb не является валидным идентификатором, поэтому компилятор выдаст ошибку). Ведут себя они одинаково, но у них есть одно тонкое идейное различие — is(typeof(expr)) не проверяет возможность компиляции, а проверяет существование типа выражения. Следовательно, теоретически, возможна ситуация, когда тип может быть известен, но по каким-либо правилам данная конструкция не может быть скомпилирована. На практике я не встречал таких ситуаций (возможно, их пока нет в языке).
Конструкция is имеет достаточно большой набор возможностей.
Есть ещё один интересный приём — pattern-matching типов.
Большая часть __traits, после ключегого слова, принимает выражение в качестве аргумента (или их список, разделённый запятыми), проверяет его результат на соответствие требованиям и возвращает булево значение, отражающее прохождение проверки. Выражения должны возвращать либо как таковой тип, либо значение типа. Другая часть принимает 1 аргумент и возвращает что-либо более информативное, нежели булево значение (в основном списки чего либо).
Проверяющие __traits:
Возвращающие что-либо:
В простейшем исполнении шаблонная функция выглядит так
Но так же у аргументов шаблонизации есть формы как и у конструкции is для проверки неявного приведения и даже для pattern-matching'а. Комбинируя это вместе с ограничениями сигнатуры можно создавать интересные комбинации перегруженных шаблонных функций:
В стандартной библиотеке по многим пакетам раскиданны template'ы, помогающие проверить поддерживает ли тип какое-либо поведение (например, необходимое для работы с функциями из этого пакета). Но есть пара пакетов, которые не реализуют какой-то специальный функционал, а предоставляют удобные обёртки над встроенными __traits и дополнительные алгоритмы проверок соответствия.
Комбинируя все эти подходы можно создавать невообразимо сложные и гибкие метапрограммные конструкции. Пожалуй в языке D реализована одна из самых гибких моделей метапрограммирования. Но всегда помните, что кто-то может потом читать этот код (может даже Вы сами) и разобраться в таких конструкциях будет очень проблематично. Всегда старайтесь соблюдать чистоту и больше комментируйте сложные моменты.
Сегодня поговорим о том, что делает метапрограммирование в D таким гибким и мощным — compile-time рефлексии. D позволяет программисту напрямую пользоваться информацией, которой оперирует компилятор, а не выводить её хитрыми способами. Так какую информацию позволяет получить компилятор и как её можно использовать?
Начнём с, наверное, самых частых в использовании, приёмов — выяснение валидности выражения:
__traits( compiles, a + b );
is( typeof( a + b ) );
И __traits(compiles, expr) и is(typeof(expr)) ждут валидное, с точки зрения лексики, выражение expr (например, выражение 12thb не является валидным идентификатором, поэтому компилятор выдаст ошибку). Ведут себя они одинаково, но у них есть одно тонкое идейное различие — is(typeof(expr)) не проверяет возможность компиляции, а проверяет существование типа выражения. Следовательно, теоретически, возможна ситуация, когда тип может быть известен, но по каким-либо правилам данная конструкция не может быть скомпилирована. На практике я не встречал таких ситуаций (возможно, их пока нет в языке).
Пример использования
Задача: создание функции, принимающей любые «похожие» на массивы объекты, содержащие «похожие» на числа элементы, возвращающей среднее значение (мат. ожидание).
Решение:
Использование:
Внимание: не используйте код из примера (isNumArray), так как он не учитывает некоторых деталей (opIndex может возвращать константную ссылку, тогда не будут возможны операции присвоения).
Решение:
template isNumArray(T)
{
enum isNumArray = __traits(compiles,
{
auto a = T.init[0]; // opIndex с int аргументом
static if( !__traits(isArithmetic,a) ) // если тип не арифметический, то он должен
{
static assert( __traits( compiles, a=a+a ) ); // складываться
static assert( __traits( compiles, a=a-a ) ); // вычитаться
static assert( __traits( compiles, a=a*.0f ) ); // умножаться на float
}
auto b = T.init.length; // свойство length
static assert( is( typeof(b) : size_t ) );
});
}
auto mean(T)( T arr ) @property if( isNumArray!T )
in { assert( arr.length > 0 ); } body
{
// уверенно можно использовать конструкцию arr[index] и arr.length
// при этом элементы, возвращаемые arr[index] будут иметь необходимые операции
auto ret = arr[0] - arr[0]; // нейтральный элемент с точки зрения сложения (0)
foreach( i; 0 .. arr.length )
ret = ret + arr[i]; // мы не проверяли перегрузку опратора +=
return ret * ( 1.0f / arr.length );
}
Использование:
import std.string : format;
struct Vec2
{
float x=0, y=0;
// перегружаем операторы сложения и вычитания
auto opBinary(string op)( auto ref const Vec2 rhs ) const
if( op == "+" || op == "-" )
{ mixin( format( "return Vec2( x %1$s rhs.x, y %1$s rhs.y );", op ) ); }
// перегружаем оператор умножения
auto opBinary(string op)( float rhs ) const
if( op == "*" )
{ return Vec2( x * rhs, y * rhs ); }
}
struct Triangle
{
Vec2 p1, p2, p3;
// перегружаем такую форму var[index]
auto opIndex(size_t v)
{
switch(v)
{
case 0: return p1;
case 1: return p2;
case 2: return p3;
default: throw new Exception( "triangle have only three elements" );
}
}
static pure size_t length() { return 3; }
}
void main()
{
auto f = [ 1.0f, 2, 3 ];
assert( f.mean == 2.0f ); // с float числами
auto v = [ Vec2(1,6), Vec2(2,7), Vec2(3,5) ];
assert( v.mean == Vec2(2,6) ); // с массивом элементов user-defined типа
auto t = Triangle( Vec2(1,6), Vec2(2,7), Vec2(3,5) );
assert( t.mean == Vec2(2,6) ); // с user-defined типом
}
Внимание: не используйте код из примера (isNumArray), так как он не учитывает некоторых деталей (opIndex может возвращать константную ссылку, тогда не будут возможны операции присвоения).
Конструкция is(… )
Конструкция is имеет достаточно большой набор возможностей.
is( T ); // проверяет семантическую валидность T
Далее тип T во всех случаях проверяется на семантическую валидность.is( T == Type ); // является ли тип T типом Type
is( T : Type ); // может ли тип T быть неявно приведён к типу Type
Существуют формы is, которые создают новые alias'ыis( T ident );
В этом случае, при валидности типа T, будет создан alias на него под именем ident. Но интересней будет комбинировать такую форму с какой либо проверкойis( T ident : Type );
is( T ident == Type );
Пример
Так же можно проверять чем является тип, выяснить его модификаторыvoid foo(T)( T value )
{
static if( is( T U : long ) ) // если тип T приводится к long
alias Num = U; // используем его
else
alias Num = long; // иначе long
}
is( T == Specialization );
В этом случае Specialization это одно из возможных значений: struct, union, class, interface, enum, function, delegate, const, immutable, shared. Соответственно проверяется является ли тип T структурой, объединением, классом и т.д. И существует форма, комбинирующая проверку и объявление alias'аis( T ident == Specialization );
Есть ещё один интересный приём — pattern-matching типов.
is( T == TypeTempl, TemplParams... );
is( T : TypeTempl, TemplParams... );
// с обявлением alias'ов
is( T ident == TypeTempl, TemplParams... );
is( T ident : TypeTempl, TemplParams... );
В этом случае TypeTempl — описание типа (составного), а TemplParams — элементы, из которых состоит TypeTempl.Пример
Вывод при компиляции
struct Foo(size_t N, T) if( N > 0 ) { T[N] data; }
struct Bar(size_t N, T) if( N > 0 ) { float[N] arr; T value; }
void func(U)( U val )
{
static if( is( U E == S!(N,T), alias S, size_t N, T ) )
{
pragma(msg, "struct like Foo: ", E );
pragma(msg, "S: ", S.stringof);
pragma(msg, "N: ", N);
pragma(msg, "T: ", T);
}
else static if( is( U T : T[X], X ) )
{
pragma(msg, "associative array T[X]: ", U );
pragma(msg, "T(value): ", T);
pragma(msg, "X(key): ", X);
}
else static if( is( U T : T[N], size_t N ) )
{
pragma(msg, "static array T[N]: ", U );
pragma(msg, "T(value): ", T);
pragma(msg, "N(length): ", N);
}
else pragma(msg, "other: ", U );
pragma(msg,"");
}
void main()
{
func( Foo!(10,double).init );
func( Bar!(12,string).init );
func( [ "hello": 23 ] );
func( [ 42: "habr" ] );
func( Foo!(8,short).init.data );
func( 0 );
}
Вывод при компиляции
struct like Foo: Foo!(10LU, double)
S: Foo(ulong N, T) if (N > 0)
N: 10LU
T: double
struct like Foo: Bar!(12LU, string)
S: Bar(ulong N, T) if (N > 0)
N: 12LU
T: string
associative array T[X]: int[string]
T(value): int
X(key): string
associative array T[X]: string[int]
T(value): string
X(key): int
static array T[N]: short[8]
T(value): short
N(length): 8LU
other: int
Конструкция __traits(keyWord, ...)
Большая часть __traits, после ключегого слова, принимает выражение в качестве аргумента (или их список, разделённый запятыми), проверяет его результат на соответствие требованиям и возвращает булево значение, отражающее прохождение проверки. Выражения должны возвращать либо как таковой тип, либо значение типа. Другая часть принимает 1 аргумент и возвращает что-либо более информативное, нежели булево значение (в основном списки чего либо).
Проверяющие __traits:
- compiles — валидно ли выражение
- isAbstractClass — абстрактные классы
- isArithmetic — арифметические типы (числа и перечисления)
- isAssociativeArray — ассоциативные массивы
- isFinalClass — финальные классы (от которых нельзя наследовать)
- isPOD — Plain Old Data — типы, которые можно инициализировать простым побайтным копированием (запрещены скрытые поля, деструкторы)
- isNested — вложенные типы (зависящие от контекста)
Примерыclass A { class B {} } pragma(msg, __traits(isNested,A.B)); // true
void f1() { auto f2() { return 12; } pragma(msg,__traits(isNested,f2)); // true }
auto f1() { auto val = 12; struct S { auto f2() { return val; } } // используется контекст f1 return S.init; } pragma(msg,__traits(isNested,typeof(f1()))); // true
- isFloating — числа с плавающей точкой (включая комплексные)
- isIntegral — целые числа
- isScalar — скалярные типы (числа, перечисления, указатели), хотя __vector(int[4]) тоже является скалярным типом
- isStaticArray — статические массивы
- isUnsigned — целые беззнаковые числа
- isVirtualMethod — виртуальный метод (то что можно перегружить)
- isVirtualFunction — виртуальные функции (те, что лежат в таблице виртуальных функций)
- isAbstractFunction — абстрактная функция
- isFinalFunction — финальная функция
- isStaticFunction — статическая функция
- isOverrideFunction — перегруженная функция
- isRef — аргумент ссылка
- isOut — выходной аргумент ссылка
- isLazy — ленивый аргумент (вычисляемый по требованию)
- isSame — являются выражения одним и тем же
- hasMember — имеет ли класс/структура такое поле/метод, принимает первым аргументом тип (или объект типа), вторым строку с именем поля/метода
Примерstruct Foo { float value; } pragma(msg, __traits(hasMember, Foo, "value")); // true pragma(msg, __traits(hasMember, Foo, "data")); // false
Про is<Some>Function и разницу между isVirtualMethod и isVirtualFunction
Для наглядности написал небольшой тест, показывающий разницу
Результат
isVirtualMethod возвращает true для всего, что можно перегрузить или уже было перегружено. Если функция не перегружалась, а изначально была final, она не будет виртуальным методом, но будет виртуальной функцией.
Насчёт знаков вопроса около лямбды и функции (литерал функционального типа) пояснить не могу, они по неведомой мне причине не прошли проверку ни на function ни на delegate.
import std.stdio, std.string;
string test(alias T)()
{
string ret;
ret ~= is( typeof(T) == delegate ) ? "D " :
is( typeof(T) == function ) ? "F " : "? ";
ret ~= __traits(isVirtualMethod,T) ? "m|" : "-|";
ret ~= __traits(isVirtualFunction,T) ? "v|" : "-|";
ret ~= __traits(isAbstractFunction,T) ? "a|" : "-|";
ret ~= __traits(isFinalFunction,T) ? "f|" : "-|";
ret ~= __traits(isStaticFunction,T) ? "s|" : "-|";
ret ~= __traits(isOverrideFunction,T) ? "o|" : "-|";
return ret;
}
class A
{
static void stat() {}
void simple1() {}
void simple2() {}
private void simple3() {}
abstract void abstr() {}
final void fnlNOver() {}
}
class B : A
{
override void simple1() {}
final override void simple2() {}
override void abstr() {}
}
class C : B
{
final override void abstr() {}
}
interface I
{
void abstr();
final void fnl() {}
}
struct S { void func(){} }
void globalFunc() {}
void main()
{
A a; B b; C c; I i; S s;
writeln( " id T m|v|a|f|s|o|" );
writeln( "--------------------------" );
writeln( " lambda: ", test!(x=>x) );
writeln( " function: ", test!((){ return 3; }) );
writeln( " delegate: ", test!((){ return b; }) );
writeln( " s.func: ", test!(s.func) );
writeln( " global: ", test!(globalFunc) );
writeln( " a.stat: ", test!(a.stat) );
writeln( " a.simple1: ", test!(a.simple1) );
writeln( " a.simple2: ", test!(a.simple2) );
writeln( " a.simple3: ", test!(a.simple3) );
writeln( " a.abstr: ", test!(a.abstr) );
writeln( "a.fnlNOver: ", test!(a.fnlNOver) );
writeln( " b.simple1: ", test!(b.simple1) );
writeln( " b.simple2: ", test!(b.simple2) );
writeln( " b.abstr: ", test!(b.abstr) );
writeln( " c.abstr: ", test!(c.abstr) );
writeln( " i.abstr: ", test!(i.abstr) );
writeln( " i.fnl: ", test!(i.fnl) );
}
Результат
id T m|v|a|f|s|o|
--------------------------
lambda: ? -|-|-|-|-|-|
function: ? -|-|-|-|s|-|
delegate: D -|-|-|-|-|-|
s.func: F -|-|-|-|-|-|
global: F -|-|-|-|s|-|
a.stat: F -|-|-|-|s|-|
a.simple1: F m|v|-|-|-|-|
a.simple2: F m|v|-|-|-|-|
a.simple3: F -|-|-|-|-|-|
a.abstr: F m|v|a|-|-|-|
a.fnlNOver: F -|v|-|f|-|-|
b.simple1: F m|v|-|-|-|o|
b.simple2: F m|v|-|f|-|o|
b.abstr: F m|v|-|-|-|o|
c.abstr: F m|v|-|f|-|o|
i.abstr: F m|v|a|-|-|-|
i.fnl: F -|-|a|f|-|-|
isVirtualMethod возвращает true для всего, что можно перегрузить или уже было перегружено. Если функция не перегружалась, а изначально была final, она не будет виртуальным методом, но будет виртуальной функцией.
Насчёт знаков вопроса около лямбды и функции (литерал функционального типа) пояснить не могу, они по неведомой мне причине не прошли проверку ни на function ни на delegate.
Возвращающие что-либо:
- identifier — прнимает один аргумент, возвращает строку (аналогичен .stringof)
- getAliasThis — принимает тип или объект типа, если у типа есть alias this, возвращает их в качестве кортежа строк, иначе пустой кортеж (насколько я помню, сейчас поддерживается только один alias this для типа)
- getAttributes — принимает идентификатор, возвращает кортеж атрибутов, объявленных пользователем (UDA — user defined attributes)
Примерenum Foo; class Bar { @(42) @Foo void func() pure @nogc @property {} } pragma(msg, __traits(getAttributes, Bar.func)); // tuple(42, (Foo)), @nogc и @property не входят в этот кортеж @Foo float value; pragma(msg, __traits(getAttributes, value)); // tuple((Foo)), работает не только с функциями
- getFunctionAttributes прнимает функцию, функциональный литерал, указатель на функцию, возвращает кортеж атрибутов в виде строк (UDA не входит сюда). Поддерживаются pure, nothrow, @?nogc, @?property, @?system, @?trusted, @?safe и ref (если функция возвращает ссылку), для классов/структур так же есть const, immutable, inout и shared. Порядок следования зависит от реализации и на него нельзя полагаться.
Примерenum Foo; class Bar { @(42) @Foo void func() pure @nogc @property {} } pragma(msg, __traits(getFunctionAttributes, Bar.func)); // tuple("pure", "@nogc", "@property", "@system")
- getMember — принимает те же аргументы, что и hasMember, эквивалентно записи через точку
Примерclass Bar { float value; } Bar bar; __traits(getMember, bar, "value") = 10; // тоже что и bar.value = 10;
- getOverloads — принимет класс/структуру/модуль и строку, совпадающую с именем функции внутри класса/структуры/модуля, возвращает кортеж всех перегрузок этой функции
Пример
Результатimport std.stdio; class A { void foo( float ) {} void foo( string ) {} int foo( int ) { return 12; } } void main() { foreach( f; __traits(getOverloads, A, "foo") ) writeln( typeof(f).stringof ); }
void(float _param_0) void(string _param_0) int(int _param_0)
- getPointerBitmap — принимает тип, возвращает массив size_t. Первое число это количество байт, занимаемое объектом этого типа, второе описывает расположение указателей, управляемых сборщиком мусора, внутри объекта такого типа
Примерclass A { // указатель на таблицу виртуальных функций, размер 1 слово, управляется GC: нет // monitor, не отмечен, размер 1, управляется GC: нет float val1; // размер 1, GC: нет A val2; // размер 1, GC: да void* val3; // размер 1, GC: да void[] val4; // размер 2 {размер GC: нет,указатель GC: да} void function() val5; // размер 1, GC: нет void delegate() val6; // размер 2 {контекст GC: да,функция GC: нет} } enum bm = 0b101011000; // ||||||||+- указатель на наблицу виртуальных функций // |||||||+-- указатель на monitor // ||||||+--- float val1 // |||||+---- A val2 // ||||+----- void* val3 // |||+------ void[] val4 размер // ||+------- void[] val4 указатель // |+-------- void function() val5 указатель // +--------- void delegate() val6 контекст // 0---------- void delegate() val6 указатель static assert( __traits(getPointerBitmap,A) == [10*size_t.sizeof, bm] ); struct B { float x, y, z; } static assert( __traits(getPointerBitmap,B) == [3*float.sizeof, 0] ); // в структуре B нет указателей, управляемых сборщиком мусора
- getProtection — принимает символ, возвращает строку, возможные варианты: «public», «private», «protected», «export» и «package»
- getVirtualMethods — принимает класс и строку с именем функции, работает практически как getOverloads, возвращает кортеж функций
- getVirtualFunctions — тоже, что и getVirtualMethods, за исключением того, что сюда входят final функции, которые ничего не перегружали
- getUnitTests — принимает класс/структуру/модуль, возвращает кортеж юниттестов как статических функций, UDA сохраняются
- parent — возвращает родительский символ, для переданного
Примерimport std.stdio; struct B { float value; void func() {} } alias F = B.func; void main() { writeln( __traits(parent,writeln).stringof ); // module stdio writeln( typeid( typeof( __traits(parent,F).value ) ) ); // float }
- classInstanceSize — принимает класс, возвращает количество байт, занимаемое экземпляром класса
- getVirtualIndex — принимает функцию (метод класса), возвращает индекс (ptrdiff_t) в таблице виртуальных функций класса. Если функция финальная и ничего не переопределяла вернёт -1
- allMembers — принимает тип и возвращает кортеж строк с именами всех полей и методов без повторений и встроенных свойств (sizeof, например), для классов включает так же поля и методы базовых классов
- derivedMembers — принимает тип и возвращает кортеж строк с именами всех полей и методов без повторений, без втроенных свойств и без полей и методов базовых классов (для классов)
Шаблонизация и ограничение сигнатуры
В простейшем исполнении шаблонная функция выглядит так
void func(T)( T val ) { ... }
Но так же у аргументов шаблонизации есть формы как и у конструкции is для проверки неявного приведения и даже для pattern-matching'а. Комбинируя это вместе с ограничениями сигнатуры можно создавать интересные комбинации перегруженных шаблонных функций:
import std.stdio;
void func(T:long)( T val ) { writeln( "number" ); }
void func(T: U[E], U, E)( T val ) if( is( E == string ) ) { writeln( "AA with string key" ); }
void func(T: U[E], U, E)( T val ) if( is( E : long ) ) { writeln( "AA with num key" ); }
void main()
{
func( 120 ); // number
func( ["hello": 12] ); // AA with string key
func( [10: 12] ); // AA with num key
}
Стандартная библиотека
В стандартной библиотеке по многим пакетам раскиданны template'ы, помогающие проверить поддерживает ли тип какое-либо поведение (например, необходимое для работы с функциями из этого пакета). Но есть пара пакетов, которые не реализуют какой-то специальный функционал, а предоставляют удобные обёртки над встроенными __traits и дополнительные алгоритмы проверок соответствия.
- std.traits — включает множество проверок и обёрток
- std.typetuple — шаблоны для работы с кортежами типов
Итог
Комбинируя все эти подходы можно создавать невообразимо сложные и гибкие метапрограммные конструкции. Пожалуй в языке D реализована одна из самых гибких моделей метапрограммирования. Но всегда помните, что кто-то может потом читать этот код (может даже Вы сами) и разобраться в таких конструкциях будет очень проблематично. Всегда старайтесь соблюдать чистоту и больше комментируйте сложные моменты.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (6)
ik62
02.07.2015 23:25Спасибо за серию статей…
Не поможете решить правильно проблему? Есть задача
- есть набор разных функций и делегатов, совпадающих по типам аргументов и результатов
- их нужно собрать в массив и применять какие-то однообразные действия(папример выполнить функию, получить результат,...)
То, что у меня получается выглядит не очень красиво. Мне не нравятся длинноты вида
X!(ReturnType!f, ParameterTypeTuple!f)(&f, «a») но я не могу найти как компактно описать одновременно тип возвращаемого значения и аргументов для функции.
И не очень нравится что раздельно описываются функции и делегаты. Теряется краткость.
Спасибо заранееdeviator Автор
03.07.2015 03:16Думаю можно обойтись только делегатами и вынести в отдельную функцию создание класса.
Правкаimport std.stdio; import std.traits; auto f(string s) { return s.length; } auto g(string s) { return 2*s.length; } struct S { auto getClosure() { return (string s) { return 3*s.length; }; } } class X(R, A...) { // три точки значат, что можно это не один тип может быть, а кортеж типов R delegate(A) g; A a; this(R delegate(A) g, A a) { this.g = g; this.a = a; } R run() { return g(a); } } auto newX(F,Args...)( F fnc, Args args ) if( isCallable!F && __traits(compiles, { fnc(args); }) ) // F может быть даже объектом с методом opCall // с помощью __traits мы проверяем можем ли вызывать эту fnc с такими аргументами { return new X!( ReturnType!F, ParameterTypeTuple!F ) // тип объекта всё равно нужно задавать ( (Args a){ return fnc(a); }, args ); // в любом случае создаём делегат } void main() { auto x = newX(&f, "a"); auto y = newX(&g, "ab"); auto s = S(); auto d = s.getClosure(); auto z = newX(d, "abc"); auto Z = [x,y,z]; foreach(t; Z) { writeln(t.run()); } }
ik62
03.07.2015 09:08Цель — написание кода для future's — это обьекты которые запускают асинхронно вызов и выполнение какой-либо функции и позволяют продолжить выполнение основной ветки кода плюс ожидание результатов future в нужный момент.
Удобно бывает собрать их в массив для того, что-бы ждать всех, собрать результат выполнения всех, и т.д. Собрать в массив можно только обьекты одного типа, поэтому обьекты future должны быть одного типа.
напримерint pause(int x) { sleep(x); return 0; } int connect(int p) { auto c = connect_to_server('127.0.0.1', p); if (c.connected) { return 0; } else { return -1; } } auto tasks = map!run([future(pause, 10), future(connect, 80)]); writeln("wait some time for results"); sleep(1); auto result = map!waitResult(tasks);
AxisPod
С++ SFINAE как-то по сравнению с этим ...., да лучше не сравнивать. По крайней мере тут шаманить не нужно. D всё же похож на первого кандидата на переход с C++, когда его комитет окончательно зарубит. Уж сильно похоже его развитие на показ своего Я и игры участников комитета.