C APIでメンバー関数にポインターを渡す

昔、C ++専用のXMPPルームで、ある訪問者が、C APIのコールバックとしてクラスのメンバー関数にポインターを渡すための特別なコードなしで、モダンプラスに方法があるかどうかを尋ねました。 さて、次のようなもの:



// C API void doWithCallback (void (*fn) (int, void*), void *userdata); // C++ code struct Foo { void doFoo (int param); }; int main () { Foo foo; doWithCallback (MAGIC (/* &Foo::doFoo */), &foo); }
      
      





MAGIC



として、無料の関数、静的なメンバー関数、またはラムダ(一般的にはヤードで2017年)を使用できることは明らかですが、各関数ごとに対応する構造を手で書くのはやや怠け者であり、私たち全員がそうであるように、プリプロセッサは、もちろん、私たちは知っています-悪いマナー。



この投稿では、ユニバーサルラッパーの作成を試みます(通常は成功します)と同時に、C ++ 17の一部の機能が冗長コードの量をさらに削減するのに役立つことを確認します。 ここには悪質なテンプレートはありませんが、私の意見では、解決策はかなり簡単ですが、おそらくそれらを共有することは理にかなっています(同時にC ++ 17の新しい機能をもう一度宣伝する)。



まず、問題をより簡単に解決します。メンバー関数が引数を受け入れないと仮定します(もちろん、暗黙のthis



を除き、これを渡す必要があります)。 典型的な正面ソリューションは次のようになります。



 doWithCallback ([] (void *udata) { return static_cast<Foo*> (udata)->doFoo (); }, &foo);
      
      





作成するドラフトコード
 #include <iostream> void doWithCallback (void (*fn) (void*), void *userdata) { fn (userdata); } struct Foo { int m_i = 0; void doFoo () { std::cout << m_i << std::endl; } }; int main () { Foo foo { 42 }; doWithCallback ([] (void *udata) { return static_cast<Foo*> (udata)->doFoo (); }, &foo); }
      
      







一般化されたラッパーを作成したい場合は、もちろん、コールバック作成の時点で、どの特定のクラスを呼び出すかを機能させる必要があります。 さらに、ラッパーは保存する必要があります。実際、正確に呼び出す必要があります。 すべてが転送で簡単な場合(メンバー関数へのポインターを取得して渡した場合)、それを保存するだけでは機能しません。



次のようなコード:



 template<typename Ptr> auto MakeWrapper (Ptr ptr) { return [ptr] (void *udata) { return (static_cast<Foo*> (udata)->*ptr) (); }; } int main () { Foo foo { 42 }; doWithCallback (MakeWrapper (&Foo::doFoo), &foo); }
      
      





空でないキャプチャリストを持つラムダは関数ポインタに変換できないため、期待どおりに収集されません。



 prog.cc:36:5: error: no matching function for call to 'doWithCallback' doWithCallback (MakeWrapper (&Foo::doFoo), &foo); ^~~~~~~~~~~~~~ prog.cc:3:6: note: candidate function not viable: no known conversion from '(lambda at prog.cc:29:12)' to 'void (*)(void *)' for 1st argument void doWithCallback (void (*fn) (void*), void *userdata) ^
      
      





これは理にかなっています:関数自体に加えて、特定のコンテキスト(または少なくともそのポインター)を保存するのに十分な「スペース」が関数ポインターにありません。



それではどうしますか? 運命ですか?



いや! 非型テンプレートパラメータが役に立ちます。圧倒的多数の場合、コールバックを転送するときに、コンパイル段階でどの関数を呼び出すかがわかります。つまり、これで特定のテンプレートをパラメータ化でき、ランタイムに情報をドラッグする必要がありません。



例えば整数でテンプレートをパラメーター化できるように、関数へのポインターでテンプレート化できます。 試してみましょう:



 template<typename R, typename C, R (C::*Ptr) ()> auto MakeWrapper () { return [] (void *udata) { return (static_cast<C*> (udata)->*Ptr) (); }; } int main () { Foo foo { 42 }; doWithCallback (MakeWrapper<void, Foo, &Foo::doFoo> (), &foo); }
      
      





動作します! ただし、1つの問題があります。戻り値の型、クラス型、および関数のパラメーター型のリスト(この場合は空です)は、毎回手で指定する必要があります。 しかし、怠は同じです。 もっと良くできますか?



C ++ 11/14では、コンパイラに上記の型を出力させることができますが、このために目的のメンバー関数を2回指定する必要があります:1回はこの関数へのポインタに対応する変数のを出力ますテンプレートの非型引数の正しい「署名」。 このようなもの:



 template<typename T> struct MakeWrapperHelper { template<typename R, typename C> static R DetectReturnImpl (R (C::*) ()); template<typename R, typename C> static C DetectClassImpl (R (C::*) ()); template<typename U> using DetectReturn = decltype (DetectReturnImpl (std::declval<U> ())); template<typename U> using DetectClass = decltype (DetectClassImpl (std::declval<U> ())); using R = DetectReturn<T>; using C = DetectClass<T>; template<R (C::*Ptr) ()> auto Make () { return [] (void *udata) { return (static_cast<C*> (udata)->*Ptr) (); }; } }; template<typename T> auto MakeWrapper (T) { return MakeWrapperHelper<T> {}; } int main () { Foo foo { 42 }; doWithCallback (MakeWrapper (&Foo::doFoo).Make<&Foo::doFoo> (), &foo); }
      
      







完全なコード
 #include <iostream> #include <tuple> void doWithCallback (void (*fn) (void*), void *userdata) { fn (userdata); } struct Foo { int m_i = 0; void doFoo () { std::cout << m_i << std::endl; } }; template<typename T> struct MakeWrapperHelper { template<typename R, typename C> static R DetectReturnImpl (R (C::*) ()); template<typename R, typename C> static C DetectClassImpl (R (C::*) ()); template<typename U> using DetectReturn = decltype (DetectReturnImpl (std::declval<U> ())); template<typename U> using DetectClass = decltype (DetectClassImpl (std::declval<U> ())); using R = DetectReturn<T>; using C = DetectClass<T>; template<R (C::*Ptr) ()> auto Make () { return [] (void *udata) { return (static_cast<C*> (udata)->*Ptr) (); }; } }; template<typename T> auto MakeWrapper (T) { return MakeWrapperHelper<T> {}; } int main () { Foo foo { 42 }; doWithCallback (MakeWrapper (&Foo::doFoo).Make<&Foo::doFoo> (), &foo); }
      
      









しかし、それはすべて恐ろしく見え、悪臭がします。 もっと良くできますか?



おそらくC ++ 14のフレームワークでそれを行うことができますが、方法はわかりませんでしたが、これができなかった証拠を見つけましたが、この記事の分野は彼には狭すぎます。



したがって、主な問題は、テンプレートの非型引数の型を明示的に指定する必要があることです。そのため、これらのすべての戻り型および他の類似のものを何らかの形で明示的に指定する必要があります。 幸いなことに、C ++ 17では、テンプレート引数の型の自動推論(これまでは開発されたclangおよびgccブランチでのみ機能していました)を追加しました。 目的のコードは大幅に簡略化されています。



 template<typename R, typename C> C DetectClassImpl (R (C::*) ()); template<auto T> auto MakeWrapper () { using C = decltype (DetectClassImpl (T)); return [] (void *udata) { return (static_cast<C*> (udata)->*T) (); }; } int main () { Foo foo { 42 }; doWithCallback (MakeWrapper<&Foo::doFoo> (), &foo); }
      
      





それだけです



いいえ、すべてではありません。 最初は、メンバー関数が任意のパラメーターセットを取得できることを思い出してください。 同様に、引数タイプのリストを返すDetectArgsImpl



を定義できます。



 template<typename R, typename C, typename... Args> std::tuple<Args...> DetectArgsImpl (R (C::*) (Args...));
      
      





部分的な専門化を利用して展開します。



 template<auto, typename> struct MakeWrapperHelper; template<auto T, typename... Args> struct MakeWrapperHelper<T, std::tuple<Args...>> { auto operator() () { using C = decltype (DetectClassImpl (T)); return [] (Args... args, void *udata) { return (static_cast<C*> (udata)->*T) (args...); }; } }; template<auto T> auto MakeWrapper () { return MakeWrapperHelper<T, decltype (DetectArgsImpl (T))> {} (); }
      
      





すべて一緒に
 #include <iostream> #include <tuple> void doWithCallback (void (*fn) (int, void*), void *userdata) { fn (7831505, userdata); } struct Foo { int m_i = 0; void doFoo (int val) { std::cout << m_i << " vs " << val << std::endl; } }; template<typename R, typename C, typename... Args> C DetectClassImpl (R (C::*) (Args...)); template<typename R, typename C, typename... Args> std::tuple<Args...> DetectArgsImpl (R (C::*) (Args...)); template<auto, typename> struct MakeWrapperHelper; template<auto T, typename... Args> struct MakeWrapperHelper<T, std::tuple<Args...>> { auto operator() () { using C = decltype (DetectClassImpl (T)); return [] (Args... args, void *udata) { return (static_cast<C*> (udata)->*T) (args...); }; } }; template<auto T> auto MakeWrapper () { return MakeWrapperHelper<T, decltype (DetectArgsImpl (T))> {} (); } int main () { Foo foo { 42 }; doWithCallback (MakeWrapper<&Foo::doFoo> (), &foo); }
      
      







そのようなこと。 Tox API、libpurple API、gstreamer API、任意のAPIを安全に使用し、大量の定型文を避けることができます。



関心のある読者のための演習として、APIによってコールバックに渡される引数の指示を追加できますが、無視する必要があります。たとえば、Toxはポインターを自分自身へのポインターとして最初の引数として渡します。



また、これらすべてからの構文のHabrahabrの色付けは悪化しているようです。



All Articles