Доброго времени суток, хабр!
Сегодня поговорим о том, что делает метапрограммирование в D таким гибким и мощным — compile-time рефлексии. D позволяет программисту напрямую пользоваться информацией, которой оперирует компилятор, а не выводить её хитрыми способами. Так какую информацию позволяет получить компилятор и как её можно использовать?
Начнём с, наверное, самых частых в использовании, приёмов — выяснение валидности выражения:
__traits( compiles, a + b ); is( typeof( a + b ) );
И __traits(compiles, expr) и is(typeof(expr)) ждут валидное, с точки зрения лексики, выражение expr (например, выражение 12thb не является валидным идентификатором, поэтому компилятор выдаст ошибку). Ведут себя они одинаково, но у них есть одно тонкое идейное различие — is(typeof(expr)) не проверяет возможность компиляции, а проверяет существование типа выражения. Следовательно, теоретически, возможна ситуация, когда тип может быть известен, но по каким-либо правилам данная конструкция не может быть скомпилирована. На практике я не встречал таких ситуаций (возможно, их пока нет в языке).
Решение:
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
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 реализована одна из самых гибких моделей метапрограммирования. Но всегда помните, что кто-то может потом читать этот код (может даже Вы сами) и разобраться в таких конструкциях будет очень проблематично. Всегда старайтесь соблюдать чистоту и больше комментируйте сложные моменты.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
ссылка на оригинал статьи http://habrahabr.ru/post/261349/
Добавить комментарий