前回の記事では、コンパイル時のリフレクションの基本要素、つまり「実際の」メタ構成が構築されるブリックについて検討しました。 この記事では、これらのトリックのいくつかを紹介します。 次のようなQtのシグナルとスロットに似たシグナルとスロットを実装してみましょう。
class Foo : XObject { @signal void message( string str ); } class Bar : XObject { @slot void print( string str ) { writefln( "Bar.print: %s", str ); } } void main() { auto a = new Foo, b = new Bar; connect( a.message, b.print ); a.message( "hello habr" ); // Bar.print: hello habr }
注意:多くのコード(コメント付き)。
おおよそですが、そうではありません=)しかし、全般的には悪くはありません。すべての理由があります。それらについてお話しします。 最後のオプション:
class Foo : XObject { mixin MixX; // , mixin @signal void _message( string str ) {} // , } class Bar : XObject { mixin MixX; // slot, void print( string str ) { writefln( "Bar.print: %s", str ); } } void main() { auto a = new Foo, b = new Bar; connect( a.signal_message, &b.print ); // a.message( "hello habr" ); // Bar.print: hello habr }
厄介なルールは、関数がmixinを介して宣言されており、同じ(ただし通常は宣言されている)単純な関数である場合、単純な関数に本体がない場合でも、mixinを介して宣言された関数は完全に単純な関数に置き換えられます。 このため、本質的には本体を持つ別の関数を宣言する必要があります。
順番に始めましょう。 最初に理解する必要があるのは、デリゲートの配列を使用したアプローチが「それほどではない」ことです。 もちろん、すべてはタスクに依存します。 このケースでは、いくつかの小さな要件があると想定しています。
- 任意のオブジェクトは有効であり、そうではありません
- オブジェクトを無効な状態に転送することができます(作成後は有効です)
- オブジェクトは子オブジェクトを持つことができます
- 親が有効でなくなった場合、子供もそのようなことをやめる
- 無効なオブジェクトの呼び出しスロットを作成しないでください(意味がありません)
論理的には、子オブジェクトは親によって完全かつ完全に所有されます。
Dでは、クラスオブジェクトはコレクターによって管理され、デストラクタはガベージコレクションが実行されるか、またはdestroy(obj)関数を使用して呼び出されます。 また、1つのポイントがあります。ガベージコレクション中はメモリを管理できません。 このため、破棄するオブジェクトをリストから削除することはできません。また、オブジェクトがそのようなリストにある間、コレクターは何もしません。 コレクターの初期要件とアイデアを考慮して、ContextHandlerの概念が必要であると結論付けました。 これが基本的なインターフェイスになります。
完全ではないが、十分に理解できるContextHandlerコード
interface ContextHandler { protected: void selfDestroyCtx(); // public: @property { ContextHandler parentCH(); // ContextHandler[] childCH(); // } final { T registerCH(T)( T obj, bool force=true ) // if( is( T == class ) ) { if( auto ch = cast(ContextHandler)obj ) if( force || ( !force && ch.parentCH is null ) ) // force - obj ... return obj; } T newCH(T,Args...)( Args args ) { return registerCH( new T(args) ); } // void destroyCtx() // { foreach( c; childCH ) // c.destroyCtx(); selfDestroyCtx(); // } } }
これは本質的にツリーです。 オブジェクトの価値を下げるとき、その子に対しても同じことを行います。 後で彼に戻ってきます。
以下の概念は、「スロット」の概念に適用されます。 スロット用に別個のUDAを作成していませんが、スロットをそのように作成するのは理にかなっています。
interface SignalConnector // { void disconnect( SlotContext ); void disonnectAll(); } class SlotContext : ContextHandler // , { mixin MixContextHandler; // ContextHandler mixin template protected: size_t[SignalConnector] signals; // , public: void connect( SignalConnector sc ) { signals[sc]++; } void disconnect( SignalConnector sc ) { if( sc in signals ) { if( signals[sc] > 0 ) signals[sc]--; else signals.remove(sc); } } protected: void selfDestroyCtx() // { foreach( sig, count; signals ) sig.disconnect(this); } } // interface SlotHandler { SlotContext slotContext() @property; } class Slot(Args...) // { protected: Func func; // SlotContext ctrl; // public: alias Func = void delegate(Args); this( SlotContext ctrl, Func func ) { this.ctrl = ctrl; this.func = func; } this( SlotHandler hndl, Func func ) { this( hndl.slotContext, func ); } void opCall( Args args ) { func( args ); } SlotContext context() @property { return ctrl; } }
すぐに信号を考慮する
class Signal(Args...) : SignalConnector, ContextHandler { mixin MixContextHandler; protected: alias TSlot = Slot!Args; TSlot[] slots; // public: TSlot connect( TSlot s ) { if( !connected(s) ) { slots ~= s; s.context.connect(this); } return s; } void disconnect( TSlot s ) // { slots = slots.filter!(a=>a !is s).array; s.context.disconnect(this); } void disconnect( SlotContext sc ) // { foreach( s; slots.map!(a=>a.context).filter!(a=> a is sc) ) s.disconnect(this); slots = slots .map!(a=>tuple(a,a.context)) .filter!(a=> a[1] !is sc) .map!(a=>a[0]) .array; } void disconnect( SlotHandler sh ) { disconnect( sh.slotContext ); } void disonnectAll() // { slots = []; foreach( s; slots ) s.context.disconnect( this ); } // void opCall( Args args ) { foreach( s; slots ) s(args); } protected: bool connected( TSlot s ) { return canFind(slots,s); } void selfDestroyCtx() { disonnectAll(); } // }
そして最後に、XBaseインターフェイスと中間XObjectクラス(MixXが挿入され、デフォルトのコンストラクターが作成されます)に到達しました。 XBaseインターフェイスは、ContextHandlerをいくつかの関数だけで拡張します。最も重要なのは、mixinテンプレートMixXです。 メタプログラミングのすべての魔法が発生します。 まず、すべてのアクションのロジックを説明する必要があります。 UDA @シグナルは、実際のシグナル関数とシグナルオブジェクト自体を作成するための基礎となる関数をマークします。 ほとんどすべては、マークされた関数から取得されます。名前(最初のアンダースコアなし)、アクセスレベル(パブリック、保護)、そしてもちろん引数です。 シグナルが任意のスロットで機能するようにしたいため、属性のうち@システムのみが許可されます。 実際のシグナル関数は、対応するシグナルオブジェクトのopCallを呼び出し、すべての引数を渡します。 新しい各クラスにすべての信号オブジェクトを作成しないように、MixXでこれを行う関数を実装します。 なぜ個別の信号関数と信号オブジェクトを作成するのですか? 信号が関数であるためには、奇妙なことに十分です。 これにより、XObjectを継承するクラスまたはXBaseを実装するクラスにインターフェイスを実装したり、信号を他の信号の呼び出しに接続したりできます。
interface Messager { void onMessage( string ); } class Drawable { abstract void onDraw(); } // class A : Drawable, XBase { mixin MixX; this() { prepareXBase(); } // @signal void _onDraw() {} } class B : A, Messager { mixin MixX; @signal void _onMessage( string msg ) {} } class Printer : XObject { mixin MixX; void print( string msg ) { } } auto a = new B; auto b = new B; auto p = new Printer; connect( a.signal_onMessage, &b.onMessage ); // connect( &p.print, b.signal_onMessage ); // connect ...
XBaseに戻ります。 コードを部分的に解析します。
interface XBase : SlotHandler, ContextHandler { public: enum signal; // UDA, enum protected: void createSlotContext(); void createSignals(); final void prepareXBase() // , XBase { createSlotContext(); createSignals(); } // XBase SlotHandler, final auto newSlot(Args...)( void delegate(Args) f ) { return newCH!(Slot!Args)( this, f ); } // , , final auto connect(Args...)( Signal!Args sig, void delegate(Args) f ) { auto ret = newSlot!Args(f); sig.connect( ret ); return ret; } mixin template MixX() { import std.traits; // ++, mixin template , static if( !is(typeof(X_BASE_IMPL)) ) { enum X_BASE_IMPL = true; mixin MixContextHandler; // ContextHandler // SlotHandler private SlotContext __slot_context; final { public SlotContext slotContext() @property { return __slot_context; } protected void createSlotContext() { __slot_context = newCH!SlotContext; } } } // mixin defineSignals; // override protected { // createSignal , static if( isAbstractFunction!createSignals ) void createSignals() { mixin( mix.createSignalsMixinString!(typeof(this)) ); } else // , createSignals void createSignals() { super.createSignals(); // mix.createSignalsMixinString , mixin( mix.createSignalsMixinString!(typeof(this)) ); } } } ... }
ミックスは、文字列を操作するすべての方法が集中している構造であることをすぐに言及する価値があります。 おそらくこれは最も成功したソリューションではありませんが、すべてを適切な場所(XBaseインターフェイス)に保ちながら、最終クラスに分類される名前の量を減らすことができます。 そして、話し始めたので、この構造を見ていきます。
static struct __MixHelper { import std.algorithm, std.array; enum NAME_RULE = "must starts with '_'"; static pure @safe: // bool testName( string s ) { return s[0] == '_'; } string getMixName( string s ) { return s[1..$]; } // , - string signalMixinString(T,alias temp)() @property { ... } // enum signal_prefix = "signal_"; // createSignals string createSignalsMixinString(T)() @property { auto signals = [ __traits(derivedMembers,T) ] .filter!(a=>a.startsWith(signal_prefix)); // , /+ signal_ + +/ return signals .map!(a=>format("%1$s = newCH!(typeof(%1$s));",a)) // signal_onSomething = newCH!(typeof(signal_onSomething); .join("\n"); // , } // template functionFmt(alias fun) if( isSomeFunction!fun ) { enum functionFmt = format( "%s %s%s", (ReturnType!fun).stringof, // __traits(identifier,fun), // (ParameterTypeTuple!fun).stringof ); // } } protected enum mix = __MixHelper.init;
MixXに戻りましょう。その中で最も難しいことは、不可欠なmixin defineSignalsです。
// @signal defineSignalsImpl mixin template defineSignals() { mixin defineSignalsImpl!( typeof(this), getFunctionsWithAttrib!( typeof(this), signal ) ); } // , ( , ) mixin template defineSignalsImpl(T,list...) { static if( list.length == 0 ) {} // else static if( list.length > 1 ) { // " " mixin defineSignalsImpl!(T,list[0..$/2]); mixin defineSignalsImpl!(T,list[$/2..$]); } else mixin( mix.signalMixinString!(T,list[0]) ); // , }
getFunctionsWithAttribとmix.signalMixinStringのパターンの複雑さはほぼ同等ですが、最初にmix.signalMixinStringを見ていきます。これは、__ MixHelperについて説明したときに切り取ったためです。
string signalMixinString(T,alias temp)() @property { enum temp_name = __traits(identifier,temp); // - enum func_name = mix.getMixName( temp_name ); // // - @system enum temp_attribs = sort([__traits(getFunctionAttributes,temp)]).array; static assert( temp_attribs == ["@system"], format( "fail Mix X for '%s': template signal function allows only @system attrib", T.stringof ) ); // , static if( __traits(hasMember,T,func_name) ) { alias base = AT!(__traits(getMember,T,func_name)); // // static assert( isAbstractFunction!base, format( "fail Mix X for '%s': target signal function '%s' must be abstract in base class", T.stringof, func_name ) ); // @system enum base_attribs = sort([__traits(getFunctionAttributes,base)]).array; static assert( temp_attribs == ["@system"], format( "fail Mix X for '%s': target signal function allows only @system attrib", T.stringof ) ); enum need_override = true; } else enum need_override = false; enum signal_name = signal_prefix ~ func_name; // alias , enum args_define = format( "alias %sArgs = ParameterTypeTuple!%s;", func_name, temp_name ); enum temp_protection = __traits(getProtection,temp); // , - enum signal_define = format( "%s Signal!(%sArgs) %s;", temp_protection, func_name, signal_name ); // , opCall enum func_impl = format( "final %1$s %2$s void %3$s(%3$sArgs args) { %4$s(args); }", (need_override ? "override" : ""), temp_protection, func_name, signal_name ); // ( ), return [args_define, signal_define, func_impl].join("\n"); }
タグ付き関数のリストの取得に戻ります。
template getFunctionsWithAttrib(T, Attr) { // <b></b>: , T // alias getFunctionsWithAttrib = impl!( __traits(derivedMembers,T) ); enum AttrName = __traits(identifier,Attr); // std.typetuple , // staticMap / anySatisfy template isAttr(A) { template isAttr(T) { enum isAttr = __traits(isSame,T,A); } } // template impl( names... ) { alias empty = TypeTuple!(); static if( names.length == 1 ) { enum name = names[0]; // , __traits(derivedMembers,T) alias, // this , static if( __traits(compiles, { alias member = AT!(__traits(getMember,T,name)); } ) ) { // : alias some = __traits(...) // template AT(alias T) { alias AT = T; } alias member = AT!(__traits(getMember,T,name)); // , alias attribs = TypeTuple!(__traits(getAttributes,member)); // static if( anySatisfy!( isAttr!Attr, attribs ) ) { enum RULE = format( "%s must be a void function", AttrName ); // static assert( isSomeFunction!member, format( "fail mix X for '%s': %s, found '%s %s' with @%s attrib", T.stringof, RULE, typeof(member).stringof, name, AttrName ) ); // - void static assert( is( ReturnType!member == void ), format( "fail mix X for '%s': %s, found '%s' with @%s attrib", T.stringof, RULE, mix.functionFmt!member, AttrName ) ); // - _ static assert( mix.testName( name ), format( "fail mix X for '%s': @%s name %s", T.stringof, mix.functionFmt!member, AttrName, mix.NAME_RULE ) ); alias impl = member; // "" } else alias impl = empty; } else alias impl = empty; } else alias impl = TypeTuple!( impl!(names[0..$/2]), impl!(names[$/2..$]) ); } }
タスクに応じて、さらにチェックを挿入できます。
接続機能を検討する必要があります。 それはメタプログラミングの背景に対してかなり奇妙に見えます:
void connect(T,Args...)( Signal!Args sig, T delegate(Args) slot ) { auto slot_handler = cast(XBase)cast(Object)(slot.ptr); // enforce( slot_handler, "slot context is not XBase" ); // , void static if( is(T==void) ) slot_handler.connect( sig, slot ); else slot_handler.connect( sig, (Args args){ slot(args); } ); } void connect(T,Args...)( T delegate(Args) slot, Signal!Args sig ) { connect( sig, slot ); }
なぜ私は信号のためにそのようなハックをしなかったのですか? たとえば、記事の冒頭にあるようにconnectを呼び出すことができます。
connect( a.message, b.print );
まず、この場合、信号とスロットの順序を修正する必要がありますが、これは名前によく反映されているはずです。 しかし、最も重要な理由:これは機能しません。 このフォーム
void connect!(alias sig, alias slot)() ...
コンテキストを保存できないため、エイリアスは基本的にClass.methodを渡します。Classは、オブジェクトではなくクラスの名前です。 そして、追加を入力する必要があります。 シグナル引数とスロット引数の対応を確認します。 デリゲートを持つフォーム
void connect(T,Args...)( void delegate(Args) sig, T delegate(Args) slot ) { ... } // connect( &a.message, &b.print );
信号を含むクラスに関する情報を失います。 関数ポインタ(sig.funcptr)でその名前を表示する方法を見つけることができませんでした。実行時に既に発生していましたが、何らかの方法でシグナルオブジェクトの名前を作成する必要があり、辞書(SignalConnector [string])になります。 したがって、実装済みとして実装されます=)
サンプルコードはgithubで、 ダブパッケージとして入手できます。