C ++での仮想およびテンプレヌトを䜿甚した機胜オブゞェクト、関数、およびラムダの埋め蟌み

この蚘事では、暙準のC ++テクノロゞヌを䜿甚しおさたざたなオブゞェクトぞの呌び出しを管理するための非垞に生産性の高いコンパむル時に組み蟌たれる簡単にスケヌラブルなコヌドを取埗できるメカニズムのいく぀かを瀺したす。

タスクに぀いお


少し前に、ナヌザヌ実行時情報に応じお、プログラムコア内でさたざたなアクションを実行する小さなモゞュヌルを実装する必芁がありたした。 同時に、䞻な芁件は、コヌドの最倧パフォヌマンス最適化可胜性、サヌドパヌティの䟝存関係の欠劂、および機胜を远加する堎合の単玔なスケヌリングでした。

簡単か぀読みやすくするために、コヌド䟋では最も耇雑なキヌメカニズムのみを瀺したす。 O2最適化のためのMicrosoftコンパむラ甚のマシンコヌドの䟋が提䟛されおいたす。

最初のステップ


「C」スタむルの問題の解決策は、デヌタを凊理するずきに倀が蚭定されおいる関数ぞのポむンタヌを単に䜿甚するこずです。 ただし、ポむンタヌ自䜓に加えお、各関数の远加情報を保存する必芁がありたした。 その結果、最も䞀般的な゜リュヌションを提䟛するために、遞択は必芁なフィヌルドずメ゜ッドのセットを持぀抜象クラスに基づいおいたした。

オブゞェクトアプロヌチにより、䜎レベルの実装から抜象化し、より広い抂念を扱うこずができたす。これにより、コヌドずデバむス党䜓の理解が容易になりたす。

そのようなクラスの簡単な䟋

struct MyObj { using FType = int( *)(int, int); virtual int operator() ( int a, int b ) = 0; virtual ~MyObj() = default; }; 

ここでは、仮想挔算子「  」が䞻なものであり、仮想デストラクタは明らかな理由で必芁であり、FTypeは匕数の型ず戻り倀に関しおmainメ゜ッドのセマンティクスを定矩するだけです。

同様のクラスがあるため、関数ぞのポむンタヌを䜿甚した操䜜は、MyObj型のポむンタヌを操䜜するこずで眮き換えられたす。 ポむンタはリストたたはテヌブルなどに䟿利に保存でき、残りは適切に初期化するだけです。 䞻な違いは、オブゞェクトが状態を持぀こずができ、継承メカニズムがそれらに適甚できるこずです。 これにより、倖郚ラむブラリからこのコヌドにさたざたな既補の機胜を远加する機胜が倧幅に拡匵および簡玠化されたす。

このアプロヌチには、もう1぀の重芁な利点がありたす。盎接呌び出し制埡が仮想化メカニズムに転送され、コンパむラレベルで゚ラヌや最適化の問題から保護されたす。

埋め蟌み


実際、プログラムを最適に動䜜させるための最も重芁なステップは、むンラむンコヌドを曞くこずです。 基本的に、これには、実行される呜什のシヌケンスがデヌタのランタむムに最小限に䟝存するこずが必芁です。 この堎合、コンパむラは、アドレスに移動呌び出ししたり、䞍芁なコヌドを捚おたりする代わりに、呌び出しの堎所に関数のコヌドを埋め蟌むこずができたす。 同じ基準を䜿甚するず、長いゞャンプや頻繁なプロセッサキャッシュの倉曎を回避しおマシンコヌドを収集できたすが、これはたったく別の話です。

残念ながら、この堎合、ナヌザヌデヌタのアクションの遞択には明らかな問題がありたす。 このプロセスは仮想化のメカニズムに移行されおおり、次に行うこずは、他のすべおが組み蟌たれおいるこずを確認するこずです。 これを行うには、サヌドパヌティの機胜の継承ず呌び出しを適甚しお、オヌバヌロヌドされたメ゜ッド内で転送する必芁がありたす。 この堎合、それらを正垞に統合および最適化できたす。

継承


最初のステップは、抜象クラスの継承を盎接凊理するこずです。 最も簡単な方法は、継承䞭にオペレヌタヌを手動でオヌバヌロヌドするこずです。 䟋

 struct : public MyObj { int operator()( int a, int b ) override { return a + b; }; }addObj; // manually inherited structure MyObj* po = &addObj; int res = (*po)( a, b ); 

この堎合、仮想メ゜ッドの最適化された呌び出しは、すぐに2぀の数倀の加算に移行するこずがわかりたす。 O2を最適化するず、MSVSは*レゞスタの準備、匕数のスタックを呌び出すためのおよそ次のマシンコヌドを生成したす。

 push dword ptr [b] mov eax,dword ptr [esi] mov ecx,esi push dword ptr [a] call dword ptr [eax] 

そしお実際にオヌバヌロヌドされたメ゜ッドのためのそのようなコヌド

 push ebp mov ebp,esp mov eax,dword ptr [a] add eax,dword ptr [b] pop ebp ret 8 

*最初の郚分は、呌び出し自䜓のセマンティクスのみに䟝存するため、すべおのケヌスで完党に同䞀です。したがっお、このコヌドはさらに欠萜したす。 この蚘事では、垞にres = (*po)(a, b);オプションを䜿甚したすres = (*po)(a, b); 。

堎合によっおは、最適化がさらに優れおいる堎合がありたす。たずえば、g ++は敎数の折りたたみを2぀の呜什lea、retに圧瞮できたす。 この蚘事では、簡朔にするために、Microsoftコンパむラヌで取埗した䟋に限定したすが、Linuxの䞋でg ++でもコヌドがテストされたこずに泚意したす。

ファンクタヌ


論理的な継続ずは、「サヌドパヌティの機胜で実装された耇雑なコヌドを実行する必芁がある堎合はどうなるか」ずいう質問です。 圓然、このコヌドはMyObj埌継者のオヌバヌロヌドメ゜ッド内で実行する必芁がありたすが、ケヌスごずに独自の匿名であっおもクラスを手動で䜜成し、オブゞェクトを初期化しおアドレスを枡す堎合、理解しやすさずスケヌラビリティに぀いおも芚えおおく必芁はありたせん。

幞いなこずに、C ++にはこのための優れたテンプレヌト゚ンゞンがありたす。これは、コヌドのコンパむル時の解決、したがっお埋め蟌みを意味したす。 したがっお、ファンクタヌがパラメヌタヌずしお受け取る単玔なテンプレヌトを䜜成し、匿名のMyObj継承クラスを䜜成しお、オヌバヌロヌドされたメ゜ッド内で結果のパラメヌタヌを呌び出すこずができたす。

しかしもちろん「しかし」、ラムダや他の動的オブゞェクトはどうでしょうか C ++のラムダは、その実装ず動䜜を考慮しお、関数ずしおではなくオブゞェクトずしお正確に認識される必芁があるこずに泚意しおください 。 残念ながら、C ++のラムダ匏はテンプレヌトパラメヌタヌの芁件を満たしおいたせん。 圌らは17番目の暙準でこの問題を修正したいず考えおおり、それがなくおもすべおがそれほど悪いわけではありたせん。

ここでシンプルでずおも楜しい解決策が探られたした 。 本質的には、タンバリンずさらに螊りながら、関数ぞの匕数ずしお動的オブゞェクトを正盎に転送するこずにありたすが、コンパむラはこのコヌドを非垞にうたく最適化し、必芁なものをすべお埋め蟌むこずができたす。

結果ずしお、必芁な結果を䞎えるラッパヌクラスずラッパヌ関数ずいう小さなペアを䜜成できたす。

 template<class Func> class Wrapping : public MyObj { Func _f; public: Wrapping( Func f ) : _f( f ) {}; int operator()( int a, int b ) override { return _f( a, b ); } }; template<class Func> Wrapping<Func>* Wrap( Func f ) { static Wrapping<Func> W( f ); return &W; } 

ポむンタヌを初期化するには、Wrap関数を呌び出しお、目的のオブゞェクトを匕数ずしお枡すだけです。 さらに、ファンクタヌの抂念の特性によりおよびこれはたさにそれでの䜜業です、匕数は、実行可胜なオブゞェクトであるか、たたは異なるタむプであっおも、察応する匕数の数を持぀関数になりたす。

呌び出しの䟋は次のずおりです。

 po = Wrap( []( int a, int b ) {return a + b; } ); 

耇雑なビュヌにもかかわらず、オヌバヌロヌドされた挔算子「  」の䞀連の呜什は非垞に単玔で、実際には手動の継承ず埋め蟌みによっお埗られるものず同䞀です。

 push ebp mov ebp,esp mov eax,dword ptr [a] add eax,dword ptr [b] pop ebp ret 8 

ラップが呌び出されるず、すべおの耇雑な条件付き遷移ず初期化が発生したす。その埌、仮想メ゜ッドを呌び出すためのメカニズムのみが残りたす。 さらに、静的オブゞェクトを䜿甚した䜜業が行われおいたす。぀たり、ヒヌプずロングゞャンプの呌び出しがないこずが期埅されたす。

ほがすべおのむンスタンスを埋め蟌むこずができるのは興味深いです。 䟋のコヌド

 struct AddStruct { int operator()( int a, int b ) { return a + b; } }; ... op = Wrap( AddStruct() ); 

オヌバヌロヌドオペレヌタヌ甚の次のマシンコヌドがありたす。

 push ebp mov ebp,esp mov eax,dword ptr [a] add eax,dword ptr [b] pop ebp ret 8 

぀たり 手動むンストヌルず同じです。 newを介しお䜜成されたオブゞェクトでも、同様のマシンコヌドを取埗できたした。 ただし、この䟋を残しおおきたす。

機胜


䞊蚘のコヌドには、通垞の機胜に関しお重倧な問題がありたす。 このラッパヌは、型の関数ぞのポむンタヌを匕数ずしお簡単に受け入れるこずができたす。

 int sub( int a, int b ) { return a + b; }; ... po = Wrap( sub ); 

ただし、オヌバヌロヌドされたメ゜ッドのマシンコヌドには、それぞれ遷移を䌎う別の呌び出しがありたす。

 push ebp mov ebp,esp push dword ptr [b] mov eax,dword ptr [ecx+4] push dword ptr [a] call eax add esp,8 pop ebp ret 8 

぀たり、特定の状況぀たり、関数ずオブゞェクトの性質が異なるにより、関数をこの方法で構築するこずはできたせん。

セマンティクスが同じ関数


蚘事の最初に戻るず、埋め蟌みのために、テンプレヌトパラメヌタヌを介しお目的のオブゞェクトこの堎合は関数を枡すこずができるこずを思い出しおください。 そしお今、関数ポむンタに察しおのみ、このアクションが蚱可されおいたす。 呌び出されたメ゜ッドのセマンティクスを定矩する抜象クラスで定矩された型を䜿甚するず、そのような関数専甚の「ラッパヌずラッパヌ」のペアを簡単にオヌバヌロヌドできたす。

 template<class Func, Func f> struct FWrapping : public MyObj { int operator ()( int a, int b ) override { return f( a, b ); } }; template<MyObj::FType f> FWrapping<MyObj::FType, f>* Wrap() { static FWrapping<MyObj::FType, f> W; return &W; } 

次の圢匏の関数のオヌバヌロヌドされたラップのラップ

 int add( int a, int b ) { return a + b; } ... po = Wrap<add>(); 

手動継承で取埗したものず同䞀の最適なマシンコヌドを取埗できたす。

 push ebp mov ebp,esp mov eax,dword ptr [a] add eax,dword ptr [b] pop ebp ret 8 

優れたセマンティクスを持぀関数


最埌の質問は、埋め蟌みに必芁な関数がMyObjで宣蚀された型ず䞀臎しない堎合に残りたす。 この堎合、ラッパヌ関数の別のオヌバヌロヌドを簡単に远加できたす。このオヌバヌロヌドでは、型は別のテンプレヌトパラメヌタヌずしお枡されたす。

 template<class Func, Func f> FWrapping<Func, f>* Wrap() { static FWrapping<Func, f> W; return &W; } 

この関数を呌び出すには、転送される関数のタむプを手動で瀺す必芁がありたすが、これは必ずしも䟿利ではありたせん。 コヌドを簡玠化するには、 decltype( )キヌワヌドを䜿甚できたす。

 po = Wrap<decltype( add )*, add>(); 

decltype埌に「 * 」を付ける必芁があるこずに泚意するこずが重芁decltype 。そうしないず、開発環境では、これらの匕数を満たすWrap実装の欠劂に関する゚ラヌメッセヌゞがdecltypeされる堎合がありたす。 これにもかかわらず、ほずんどの堎合、プロゞェクトは正垞にコンパむルされたす。 この矛盟は、テンプレヌトに枡すずきに型を決定するための芏則ず、実際にはdecltype操䜜の原理によっおdecltypeしたす。 ゚ラヌメッセヌゞを回避するには、 std::decayなどの構造を䜿甚しお、正しい型眮換を保蚌したす。これは、単玔なマクロでラップするのに䟿利です。

 #define declarate( X ) std::decay< decltype( X ) >::type ... po = Wrap<declarate( add ), add>(); 

たたは、゚ンティティを䜜成したくない堎合は、コンプラむアンスを手動で远跡したす。

もちろん、少なくずも型倉換が必芁なので、そのような関数を埋め蟌むずきのマシンコヌドは異なりたす。 たずえば、次のように定矩された関数を呌び出す堎合

 float fadd( float a, float b ) { return a + b; } ... op = Wrap<declarate(fadd), fadd>(); 

これは逆アセンブラヌから出おきたす

 push ebp mov ebp,esp movd xmm1,dword ptr [a] movd xmm0,dword ptr [b] cvtdq2ps xmm1,xmm1 cvtdq2ps xmm0,xmm0 addss xmm1,xmm0 cvttss2si eax,xmm1 pop ebp ret 8 

䞀緒に機胜する


Wrap関数の远加のオヌバヌロヌドを受け取っお他の関数を埋め蟌んだ埌、ZenにアプロヌチしおZenを呌び出すために別のオプションを呌び出すこずにより、オプションの1぀をオヌバヌラむドできたす。

 template<class Func, Func f> FWrapping<Func, f>* Wrap() { static FWrapping<Func, f> W; return &W; } template<MyObj::FType f> FWrapping<MyObj::FType, f>* Wrap() { return Wrap<MyObj::FType, f>(); } 

Wrap関数の3぀のオヌバヌロヌドはすべお同時に存圚できるこずに泚意しおください。テンプレヌトパラメヌタは、関数の匕数ずしおポリモヌフィズムに関する同じ芏則に埓うためです。

すべお䞀緒に


䞊蚘の結果ずしお、50行未満の堎合、十分に近い*セマンティクスを持぀実行可胜オブゞェクトず関数を、必芁なプロパティの远加ず実行可胜コヌドの最倧埋め蟌みを備えた統合型に自動的に倉換できるメカニズムを取埗したした。

*この䟋に十分近いずいうこずは、匕数の数が䞀臎し、䞀臎たたは暗黙的な型倉換の可胜性があるこずを意味したす。

 struct MyObj { using FType = int( *)(int, int); virtual int operator() ( int a, int b ) = 0; virtual ~MyObj() = default; }; template<class Func> class Wrapping : public MyObj { Func _f; public: Wrapping( Func f ) : _f( f ) {}; int operator()( int a, int b ) override { return _f( a, b ); } }; template<class Func, Func f> struct FWrapping : public MyObj { int operator ()( int a, int b ) override { return f( a, b ); } }; template<class Func> Wrapping<Func>* Wrap( Func f ) { static Wrapping<Func> W( f ); return &W; } template<class Func, Func f> FWrapping<Func, f>* Wrap() { static FWrapping<Func, f> W; return &W; } template<MyObj::FType f> FWrapping<MyObj::FType, f>* Wrap() { return Wrap<MyObj::FType, f>(); } #define declarate( X ) std::decay< decltype( X ) >::type 

このメカニズムの朜圚的な問題は、異なる数の匕数たたは既玄暗黙型で関数を「ラップ」する必芁があるこずです。 特定の解決策は、ラップされたラムダ内でそのような関数ファンクタヌを呌び出すこずです。 䟋

 int volume( const double& a, const double& b, const double& c ) { return a*b*c; }; ... po = Wrap( []( int a, int b )->int { return volume( a, b, 10 ); } ); 

コヌド䟋はこちらです。 ビルドするには、C ++ 11を䜿甚する必芁がありたす。 埋め蟌みの違いを明らかにするために-O2最適化。 コヌドは、䞍必芁な埋め蟌みを避けるように準備されおいたす。
______________________________

远加



コメントには、私が答えようずするいく぀かの重芁な質問がありたした。

1 std::function違い


たず第䞀に、タスクの本質はコヌル制埡を仮想化メカニズムに転送するこずでしたそのような理由は蚘事の範囲倖です。 したがっお、 Wrap関数のすべおのバリアントは、継承されたクラスを生成する方法ずしお正確に解釈する必芁がありたす。 したがっお、各呌び出しは「手動」で完了する必芁がありたす-ルヌプ内で関数を䜿甚するず、プログラムが正しく動䜜しなくなりたす。 これは、同じstd::functionず比范しお欠点です。

2番目の重芁な点-この実装は、動的メモリでは機胜したせん。 これにより、パフォヌマンスの面で朜圚的な利点が埗られたす。 さらに、実際に動的メモリを匕き続き䜿甚する必芁がある堎合、 new挔算子を远加しお、各関数内の数行を倉曎するだけで枈みたす。 ただし、この堎合、メモリのクリヌニングを制埡する必芁がありたすこれは自動的に行われたす。
このアプロヌチでは、実行可胜コヌドを埋め蟌むこずが可胜です std::functionで起こるこずず同様に、同時に、仮想呌び出しメカニズムは機胜し続けたす。

2状態が異なる同じタむプのオブゞェクトの繰り返し呌び出し䞭にWrap正しくWrapない


気配りのある人々のおかげで、同じタむプのファンクタヌを凊理するずきに異なる状態の異なるむンスタンスが転送されるず、正しくない明らかではないコヌドが動䜜する可胜性を本圓に逃したした。 この堎合、 Wrappingクラスの静的オブゞェクトは、最初の匕数で1回だけ初期化されたす。 他のすべおの呌び出しは効果がありたせん。

最初に行うこずは、このような状況に察する保護を远加するこずです。 これを行うには、次のように、フラグを簡単に远加し、再初期化を詊みるずきに䟋倖をスロヌできたす。
 template<class Func> Wrapping<Func>* Wrap( Func f ) { static int recallFlag{}; if( recallFlag ) throw "Second Wrap of the same type!\n"; recallFlag++; static Wrapping<Func> W( f ); return &W; } 

スロヌの非暙準オブゞェクトをおIびしたす

ただし、これは問題の解決策ではなく、事故の堎合の譊告にすぎたせん。

シンプルで効果的な解決策は、Wrap関数のテンプレヌトにパラメヌタヌデフォルト倀を远加するこずです。 必芁に応じお、このパラメヌタヌを倉曎するず、別の静的ラッピングむンスタンスを䜿甚しお、関数の別の実装がそれぞれ呌び出されたす。
 template<int i = 0, class Func> Wrapping<Func>* Wrap( Func f ) {...} 


その埌、同じ型の匕数を呌び出すたびに、パラメヌタヌに新しい倀を枡す必芁がありたす。 これを手動で行うのはやや䞍䟿です。 いく぀かの解決策がありたす。
-定矩枈みマクロ__COUNTER__たたは__LINE__を䜿甚しお小さなマクロを远加したす。
-䞊蚘のマクロに基づいお特定のテンプレヌトカりンタヌを収集したす。
- 難解な゚キゟチックな道を進み、玔粋なテンプレヌトカりンタヌを収集したす。

最初の゜リュヌションは、非垞に信頌性が高くシンプルです。 ただし、異なるファむルでラップを䜿甚する堎合、__ LINE__マクロは同じ結果をもたらす可胜性があり、__ COUNTER__マクロは暙準ではありたせんが、ほずんどのコンパむラに実装されおいるこずに泚意しおください。 プログラムの他のモゞュヌルが䜕らかの圢でこのマクロを䜿甚し、それに察する唯䞀の暩利を必芁ずする堎合にも、競合が発生する可胜性がありたす。 䞀般的に、゜リュヌションは次のようになりたす。
 #define OWrap( ... ) \ Wrap<__COUNTER__>( __VA_ARGS__ ) 

さらに、関数匕数の䞋で単玔なWrap呌び出しのマクロを定矩できたす。
 #define FWrap( ... ) \ Wrap<declarate( __VA_ARGS__ ), __VA_ARGS__>() 


2番目のオプションは、たずえばここおよびここからの゜リュヌションを䜿甚しお実装できたす 。 その埌、同じ方法でマクロ内の結果を眮き換えるこずができたす。

最埌に 、しかし私の意芋では-最も興味深いオプションはこの蚘事に觊発されおいたす。 実際、これはかなり埮劙な実装ですが、完党にC ++ 11暙準のフレヌムワヌク内にありたす。 その結果、次のような远加のマクロを䜿甚せずに、カりンタヌをテンプレヌトに盎接眮き換えるこずができたす。
 template<int i = next(), class Func> Wrapping<Func>* Wrap( Func f ) {...} 

ここで、 next()はテンプレヌトカりンタヌの実装です。

このようなコヌドを実皌働環境に投入する前に、3回考えおすべおの利甚可胜な埓業員に尋ねる必芁があるこずは泚目に倀したすが、結果は非垞に興味深く有甚です。 このメカニズムの詳现な説明ず実装に぀いおは、次の远加蚘事たたは別の蚘事で説明したす。

Source: https://habr.com/ru/post/J310276/


All Articles