今日は、Dでのメタプログラミングが非常に柔軟で強力なもの、つまりコンパイル時のリフレクションについて説明します。 Dを使用すると、プログラマーは、コンパイラーが操作する情報を、トリッキーな方法で表示するのではなく、直接使用できます。 では、コンパイラはどのような情報を取得し、どのように使用できますか?
おそらく最も一般的な使用方法から始めましょう-式の妥当性を調べる:
__traits( compiles, a + b ); is( typeof( a + b ) );
__traits(compiles、expr)とis(typeof(expr))は、語彙の観点から有効な式exprを期待します(たとえば、式12thbは有効な識別子ではないため、コンパイラーはエラーをスローします)。 それらは同じように動作しますが、1つの微妙なイデオロギーの違いがあります-is(typeof(expr))はコンパイル能力をチェックしませんが、式のタイプの存在をチェックします。 したがって、理論的には、型を知ることができる状況は可能ですが、いくつかの規則によってこの構造をコンパイルすることはできません。 実際には、私はそのような状況に遭遇したことはありません(おそらくそれらはまだ言語になっていないでしょう)。
使用例
タスク:数値に「類似する」要素を含む配列に「類似する」オブジェクトを受け入れる関数を作成し、平均値(mat。Expectation)を返します。
解決策:
使用法:
警告 :例(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=aa ) ); // 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( T ); // T
さらに、すべての場合において、タイプTの意味的妥当性がチェックされます。
is( T == Type ); // T Type is( T : Type ); // T Type
新しいエイリアスを作成するフォームがあります。
is( T ident );
この場合、タイプTが有効であれば、identという名前でエイリアスが作成されます。 しかし、そのようなフォームを何らかの検証と組み合わせることはより興味深いでしょう
is( T ident : Type ); is( T ident == Type );
例
また、タイプが何であるかを確認し、その修飾子を見つけることができます
void foo(T)( T value ) { static if( is( TU : long ) ) // T long alias Num = U; // else alias Num = long; // long }
is( T == Specialization );
この場合、Specializationは可能な値の1つです:struct、union、class、interface、enum、function、delegate、const、immutable、shared。 したがって、タイプTが構造体、共用体、クラスなどであるかどうかがチェックされます。 そして、検証とエイリアスの宣言を組み合わせたフォームがあります
is( T ident == Specialization );
別の興味深いトリックがあります-パターンマッチングタイプです。
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( UE == 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( UT : T[X], X ) ) { pragma(msg, "associative array T[X]: ", U ); pragma(msg, "T(value): ", T); pragma(msg, "X(key): ", X); } else static if( is( UT : 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の検証:
- コンパイル-有効な式
- isAbstractClass-抽象クラス
- isArithmetic-算術型(数値と列挙)
- isAssociativeArray-連想配列
- isFinalClass-最終クラス(継承できません)
- isPOD-プレーンオールドデータ-単純なバイトコピーで初期化できるタイプ(非表示フィールド、デストラクタは禁止されています)
- isNested-ネストされたタイプ(コンテキスト依存)
例class A { class B {} } pragma(msg, __traits(isNested,AB)); // 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-クラス/構造にそのようなフィールド/メソッドがあり、最初の引数で型(または型オブジェクト)を受け入れ、2番目の行でフィールド/メソッド名を受け入れます
例struct Foo { float value; } pragma(msg, __traits(hasMember, Foo, "value")); // true pragma(msg, __traits(hasMember, Foo, "data")); // false
is <Some>関数とisVirtualMethodとisVirtualFunctionの違いについて
明確にするために、違いを示す小さなテストを作成しました
結果
isVirtualMethodは、リロードできるもの、またはすでにオーバーロードされているものに対してtrueを返します。 関数がオーバーロードされておらず、元々最終的なものであった場合、それは仮想メソッドではなく、仮想関数になります。
ラムダと関数(関数型のリテラル)についての疑問符については説明できませんが、不明な理由により、関数またはデリゲートのいずれのテストにも合格しませんでした。
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を返します。 関数がオーバーロードされておらず、元々最終的なものであった場合、それは仮想メソッドではなく、仮想関数になります。
ラムダと関数(関数型のリテラル)についての疑問符については説明できませんが、不明な理由により、関数またはデリゲートのいずれのテストにも合格しませんでした。
何かを返す:
- identifier-1つの引数を取り、文字列を返します(.stringofと同様)
- getAliasThis-型に型thisがある場合、型または型のオブジェクトを受け入れ、文字列のタプルとして返します。そうでない場合は空のタプル(覚えている限り、これは型に対して1つのエイリアスのみサポートされます)
- getAttributes-識別子を受け入れ、ユーザーが宣言した属性のタプルを返します(UDA-ユーザー定義の属性)
例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を返します。 最初の数はこのタイプのオブジェクトが占めるバイト数であり、2番目はこのタイプのオブジェクト内のガベージコレクターによって管理されるポインターの位置を示します
例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と同じですが、何もオーバーロードしなかった最終関数が含まれています。
- 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 ) { ... }
しかし、同じように、テンプレート引数には、暗黙的なキャストをチェックするための、さらにはパターンマッチングのための構造もあります。 これと署名の制限を組み合わせて、オーバーロードされたテンプレート関数の興味深い組み合わせを作成できます。
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 }
標準ライブラリ
多くのパッケージの標準ライブラリには、型が何らかの動作をサポートするかどうかを確認するためにテンプレートが散在しています(たとえば、このパッケージの関数を使用するために必要です)。 ただし、特別な機能を実装しないパッケージがいくつかありますが、組み込みの__traitsの便利なラッパーと、コンプライアンスをチェックするための追加のアルゴリズムを提供します。
- std.traits-多くのチェックとラッパーが含まれています
- std.typetuple-タイプタプルを操作するためのテンプレート
まとめ
これらのアプローチをすべて組み合わせて、想像を絶するほど複雑で柔軟なメタプログラム構成を作成できます。 おそらくD言語は、最も柔軟なメタプログラミングモデルの1つを実装しています。 しかし、誰かがこのコード(おそらくあなた自身)を読み、そのような構成が非常に問題になることを理解できることを常に覚えておいてください。 常にきれいになり、困難な瞬間にもっとコメントするようにしてください。