リンクまたはシンプルなメッセージングの自由について

メッセージングは​​、 コンピュータサイエンスのかなり基本的なものです。 イベント駆動型プログラミング( イベント駆動型 )により近いものと考えます。 用語、機能、および実装は異なる場合があります:イベント( イベント )、メッセージ( メッセージ )、シグナル/スロット( シグナル/スロット )およびコールバック 。 一般的に、本質は、イベントの発生とともに、応答がトリガーされるということです。

この記事のメッセージングシステム自体は、リンク/ポインターの無料で許容可能な解釈のデモンストレーションとして機能し、コードを簡素化しました。 結果として得られるシステムは簡単で、特定のメッセージコードのハンドラーを登録し、そのようなコードでメッセージを送信できます。

ハンドラーは自明ではなく、メッセージはほとんどないとしましょう。 そして、私たち自身がメッセージを生成し、それらが例えばネットワークを介して私たちに届かないこと。 この場合、メッセージ内の明示的な変数宣言を使用して、より便利なものが欲しいです。 たとえば、次のようなもの:

StringMessage* str_message = ...; send(my_message); ... void handle_message(const Message* message) { assert(message); const StringMessage* str_message = dynamic_cast<const StringMessage*>(message); assert(str_message); std::cout << str_message->message ... }
      
      





しかし、私は内部で作業のロジックに関係しない検証コードを削除したいと思います。 したがって、リンクへのポインターを置き換えて、 NULL nullptrではなく、オブジェクトが確実にハンドラーに入ることを示します 。 そして、ハンドラーが必要な種類のメッセージをすぐに受け入れるようにします。

 void handle_message(const StringMessage& message) { ... }
      
      





計画を実装し、他の可能なメッセージクラスをサポートする方法



アイデアはシンプルです。 ハンドラーの登録時に、取る引数のタイプを見つけて、それを記述します。 そして、メッセージを送信するとき、メッセージタイプがハンドラー引数のタイプと一致することを確認します。 新しいメッセージタイプごとに、 Message



メッセージの基本クラスから継承します。

 class Message{ public: Message(unsigned code) : code(code) {} virtual ~Message() {} const unsigned code; }; enum Code { STRING = 1 }; class StringMessage : public Message { public: StringMessage(const std::string& msg) : Message(STRING), message(msg) {} const std::string message; };
      
      







代表者との決定



古き良きデリゲートはC ++ 03で動作します。 実装の例の1つは、 ここ Habré 説明されています 。 この場合のデリゲートは、メンバー関数の単なる機能ラッパーです。 これがハンドラーサブスクリプションの外観です。

 class Messenger { ... template <class T, class MessageDerived> void subscribe(int code, T* object, void (T::* method)(const MessageDerived&)) { //   ,    -  const std::type_index& arg_type = typeid(const MessageDerived); //   ,      (const Message&) void (T::* sign)(const Message&) = (void (T::*)(const MessageDerived&)) method; //    subscribers_.push_back(Subscriber(code, object, NewDelegate(object, sign), arg_type)); } }
      
      





正確さ。 派生メッセージクラスのデバイスの重要性が低下すると、 オブジェクトトリミングの問題が発生します。 sendメソッドを入力すると、送信された参照をベースオブジェクトにシフトすることにより、オブジェクトはベースタイプに切り捨てられます。 ハンドラーはこれを認識せず、無効なリンクを使用します。 このようなオブジェクトに遭遇した場合はお知らせします。

 template <class Base, class Derived> bool is_sliced(const Derived* der) { return (void*) der != (const Base*) der; }
      
      





ただし、コンパイル時のチェックを作成することをお勧めします。 コンパイラーは、継承されたものに従って基本型のスライスを作成します。 ポインターが1から増加した場合、オブジェクトはカットされました。

 template <class Base, class Derived> struct is_sliced2 : public std::integral_constant<bool, ((void*)((Base*)((Derived*) 1))) != (void*)1> {}; ... static_assert(!is_sliced2<Message, Arg>::value, "Message object should not be sliced");
      
      





残念ながら、MSVS 2013コンパイラーは条件のコンパイルに対応していませんが、gcc-4.8.1 問題ありません。



メッセージの送信は簡単です。 メッセージが切り捨てられていないことを確認してください。 すべてのハンドラーを実行します。 メッセージコードとハンドラーが一致する場合、コンプライアンスのタイプを確認します。 すべてが一致したら、ハンドラーを呼び出します。

メッセージ送信
 class Messenger { ... template <class LikeMessage> void send(const LikeMessage& msg) { assert((!is_sliced<Message, LikeMessage>(&msg))); send_impl(msg); } private: void send_impl(const Message& msg) { const std::type_info& arg = typeid(msg); //     for (SubscribersCI i = subscribers_.begin(); i != subscribers_.end(); ++i) { if (i->code == msg.code) { //    if (arg != i->arg_type) // ,        throw std::logic_error("Bad message cast"); i->method->call(msg); //  -  } } } }
      
      







MessageDerived



実際にMessage



から継承されていることを確認することを忘れないでください。 C ++ 11では、 <type_traits>



ファイルにはstd::is_base_of



ます。 C ++ 03では、コンパイル時間を手動で記述する必要があります。

デリゲートの例は簡単です。 ハンドラークラス、デリゲートサブスクリプション、およびメッセージ送信:

 class Printer { public: void print(const StringMessage& msg) { std::cout << "Printer received: " << msg.message << std::endl; } }; int main() { Messenger messenger; Printer print; messenger.subscribe(STRING, &print, &Printer::print); messenger.send(StringMessage("Hello, messages!")); return 0; }
      
      





デリゲートを使用したコード



C ++ 11



ラムダはC ++ 11で登場しました。 私たちの目標は、サブスクリプションプロセスを非常にシンプルに見せることです。

 messenger.subscribe(STRING, [](const StringMessage& msg) {...});
      
      





std::function



でラムダをラップできますが、入力引数のタイプを失うことなくラムダのタイプを知る必要があります。 そして、ラムダをstd::function<void (const Message&)>



ような普遍的なものに変換します。 しかし、単にそれを取得してC ++ラムダのタイプを見つけることはできません。

ラムダの種類を調べる
 template <typename Function> struct function_traits : public function_traits<decltype(&Function::operator())> {}; template <typename ClassType, typename ReturnType, typename... Args> struct function_traits<ReturnType(ClassType::*)(Args...) const> { typedef ReturnType (*pointer)(Args...); typedef std::function<ReturnType(Args...)> function; };
      
      





ここから借りました 。 理解できない、再帰的に継承されたものであり、部分的な専門化さえあります! しかし、ポイントは、各ラムダが呼び出しに使用されるoperator()



持っていることです。 decltype(&Function::operator())



は、これをラムダに対応するメンバー関数の型に展開します。 引数は部分的に特殊化されたテンプレートに渡され、対応する同義語が関数ポインターのタイプに設定され、 std::function



関数ポインターに設定されます。



コードは、デリゲートを使用したオプションと意味が似ています。 ラムダを使用するロジックのみが複雑です。

 template <typename Function> class Messenger { ... void subscribe(int code, Function func) { //      function_traits typedef typename function_traits<Function>::function FType; //  std::function    argument_type (  ) typedef typename FType::argument_type Arg; //  typeid  auto& arg_type = typeid(Arg); // ,     Message //  Arg  .   ,     . typedef std::remove_reference<Arg>::type ArgNoRef; //    static_assert(std::is_base_of<Message, ArgNoRef>::value, "Argument type not derived from base Message"); //         auto ptr = to_function_pointer(func); //        ,    auto pass = (void(*) (const Message&)) ptr; subscribers_.emplace_back(std::move(Subscriber(code, pass, arg_type))); } }
      
      





to_function_pointerの中には何がありますか?
ラムダは、対応する型の関数へのポインター型に静的に変換されます。

 template <typename Function> typename function_traits<Function>::pointer to_function_pointer(Function& lambda) { return static_cast<typename function_traits<Function>::pointer>(lambda); }
      
      







ご注意
反対方向のキャストを作成する方がはるかに簡単であることは注目に値します。

 std::function<void (const Message&)> msg_func = ...; std::function<void (const StringMessage&)> str_func = msg_func; //  
      
      





パブリック継承はis関係の実装であるため、これは論理的な動作です。 具体的には、 StringMessage



Message



です。 しかし、その逆ではありません。



送信コードは、ほぼ逐語的に、解析されたコードをデリゲートで繰り返します。 lambdasを使用したすべてのコード

これが私たちの仕事の終わりです。 登録して、メッセージを送信して処理するだけです。

 int main() { Messenger messenger; messenger.subscribe(STRING, [](const StringMessage& msg) { std::cout << "Received: " << msg.message << std::endl; }); messenger.send(StringMessage("Hello, messages!")); return 0; }
      
      





また、いくつかの引数に対するコールバックのより一般的な実装を含む記事へのリンクも提供します。



パフォーマンスドローダウン



パフォーマンスがどれだけ低下したかを見てみましょう。 2つのインスタントメッセンジャーに対してハンドラーを1つだけ使用してみましょう。そのうちの1つは私たちのもので、 Message



から継承した任意のタイプを受け入れることができます。 そして、 StringMessage



文字列を持つメッセージのみを受け入れることができる2番目のもの。 1つの確立されたメッセージを5億回送信します。

 Msg: 13955ms Str: 1176ms Ratio: 12.0
      
      





12倍遅くなります。 すべての違いは、単一のメッセージに送信するときに引数のtypeid



型を取得し、型の一致を確認することです。 この数字は気のめいるようですが、それについては覚えていますが、それでも最も重要な数字ではありません。 おそらくボトルネックは、メッセージを送信するプロセスではなく、プログラムの処理で発生するためです。 最悪の場合、リリースモードで型チェックを削除して、パフォーマンスを調整できます。

測定コード

私が黙っていたこと
Lambdの削除の問題には対処しませんでした。 デリゲート付きのバージョンでは、オブジェクトへのポインターを保持し、オブジェクトを削除するときに、サブスクライバーに関するすべての情報を削除できます。 ここでは、同じことを行い、サブスクリプションメソッドへのオブジェクトポインターに別の引数を追加することを除いて、他のソリューションは表示されません。



まとめ



その結果、シンプルで非常に便利なプロトタイプメッセージングシステムを手に入れました。 すべてのコードはGitHubで入手できます



All Articles