1

私はC++で信号とスロットシステムを設計しようとしています。このメカニズムは、boost :: signalに多少影響を受けていますが、もっと単純なはずです。私はMSVC2010を使用しています。つまり、一部のc ++ 11機能は使用できますが、残念ながら可変個引数テンプレートは使用できません。

まず、コンテキスト情報をいくつか示します。PCに接続されたさまざまなハードウェアセンサーによって生成されたデータを処理するためのシステムを実装しました。すべての単一のハードウェアセンサーは、ジェネリッククラスDeviceから継承するクラスによって表されます。すべてのセンサーは、データを受信し、それを複数のプロセッサークラス(フィルター、ビジュアライザーなど)に転送できる個別のスレッドとして実行されます。つまり、デバイスは信号であり、プロセッサはスロットまたはリスナーです。センサーによって大量のデータが生成されるため、信号/スロットシステム全体が非常に効率的である必要があります。

次のコードは、1つの引数を持つシグナルに対する私の最初のアプローチを示しています。より多くの引数のサポートを含めるために、より多くのテンプレートの特殊化を追加(コピー)できます。以下のコードでは、これまでのところスレッドセーフが欠落しています(slots_vecへのアクセスを同期するにはミューテックスが必要です)。

スロットのすべてのインスタンス(つまり、プロセッサインスタンス)が別のスレッドで使用できないようにしたかったのです。したがって、unique_ptrとstd :: moveを使用して、スロットの移動セマンティクスを実装することにしました。これにより、スロットが切断された場合、または信号が破壊された場合にのみ、スロットも破壊されるようにする必要があります。

これが「エレガントな」アプローチかどうか疑問に思います。以下のSignalクラスを使用するクラスは、Signalのインスタンスを作成するか、Signalから継承して、一般的なメソッド(つまり、接続、発行など)を提供できるようになりました。

#include <memory>
#include <utility>
#include <vector>

template<typename FunType>
struct FunParams;

template<typename R, typename A1>
struct FunParams<R(A1)>
{
    typedef R Ret_type;
    typedef A1 Arg1_type;
};

template<typename R, typename A1, typename A2>
struct FunParams<R(A1, A2)>
{
    typedef R Ret_type;
    typedef A1 Arg1_type;
    typedef A2 Arg2_type;
};


/**
Signal class for 1 argument.
@tparam FunSig Signature of the Signal
*/
template<class FunSig>
class Signal
{
public:
    // ignore return type -> return type of signal is void
    //typedef typenamen FunParams<FunSig>::Ret_type Ret_type;
    typedef typename FunParams<FunSig>::Arg1_type Arg1_type;

    typedef typename Slot<FunSig> Slot_type;

public:
    // virtual destructor to allow subclassing
    virtual ~Signal()
    {
        disconnectAllSlots();
    }

    // move semantics for slots
    bool moveAndConnectSlot(std::unique_ptr<Slot_type> >& ptrSlot)
    {
        slotsVec_.push_back(std::move(ptrSlot));
    }

    void disconnectAllSlots()
    {
        slotsVec_.clear();
    }

    // emit signal
    void operator()(Arg1_type arg1)
    {
        std::vector<std::unique_ptr<Slot_type> >::iterator iter = slotsVec_.begin();
        while (iter != slotsVec_.end())
        {
            (*iter)->operator()(arg1);
            ++iter;
        }
    }

private:
    std::vector<std::unique_ptr<Slot_type> > slotsVec_;

};


template <class FunSig>
class Slot
{
public:
    typedef typename FunParams<FunSig>::Ret_type Ret_type;
    typedef typename FunParams<FunSig>::Arg1_type Arg1_type;

public:
    // virtual destructor to allow subclassing
    virtual ~Slot() {}

    virtual Ret_type operator()(Arg1_type) = 0;
};

このアプローチに関するその他の質問:

1)通常、シグナルとスロットは、引数として複雑なデータ型へのconst参照を使用します。boost :: signalでは、参照をフィードするためにboost::crefを使用する必要があります。それは避けたいと思います。次のようにSignalインスタンスとSlotインスタンスを作成した場合、引数がconst refsとして渡されることが保証されていますか?

class Sens1: public Signal<void(const float&)>
{
  //...
};

class SpecSlot: public Slot<Sens1::Slot_type>
{
   void operator()(const float& f){/* ... */}
};

Sens1 sens1;
sens1.moveAndConnectSlot(std::unique_ptr<SpecSlot>(new SpecSlot));
float i;
sens1(i);

2)boost :: signal2はスロットタイプを必要としません(レシーバーは一般的なスロットタイプから継承する必要はありません)。実際には、任意のファンクターまたは関数ポインターを接続できます。これは実際にどのように機能しますか?これは、boost::functionを使用して関数ポインターまたはメソッドポインターをシグナルに接続する場合に役立つことがあります。

4

2 に答える 2

2

前提:

これを大規模なプロジェクトまたは本番プロジェクトで使用する予定がある場合、私の最初の提案は、車輪を再発明するのではなく、Boost.Signals2 または代替ライブラリを使用することです。これらのライブラリは、あなたが思っているほど複雑ではなく、あなたが思いつくアドホックなソリューションよりも効率的である可能性があります。

これは、あなたの目標がより教訓的なものであり、それらがどのように実現されるかを理解するためにこれらのことを少し試してみたい場合、私はあなたの精神に感謝し、あなたの質問に答えようとしますが、いくつかを与える前ではありません.改善のためのアドバイス

アドバイス:

まず、この文は紛らわしいです。

"接続および切断メソッドは、これまでのところスレッド セーフではありません。しかし、スロットのすべてのインスタンス (つまり、プロセッサ インスタンス) を別のスレッドで使用できないようにしたかったので、スロットの移動セマンティクスを使用unique_ptrして実装することにしました。 std::move" .

あなたがそれについて考えている場合に備えて(文中の「しかし」はそれを示唆しています)、を使用しても、スロットをデータ競合unique_ptrから保護する必要がなくなるわけではありません。したがって、とにかくvectorアクセスを同期するにはミューテックスを使用する必要があります。slots_vec

2 番目のポイント: を使用してunique_ptr、スロット オブジェクトの排他的所有権を個々のシグナル オブジェクトに与えます。私の理解が正しければ、異なるスレッドが同じスロットを台無しにするのを避けるためにこれを行っていると主張しています(これにより、アクセスを同期する必要があります)。

これが設計上、合理的な選択であるかどうかはわかりません。まず第一に、複数の信号に対して同じスロットを登録することが不可能になります (は必要ないと反対していると聞きますが、待ってください)。第 2 に、これらのプロセッサの状態を実行時に変更して、プロセッサが受信したシグナルに反応するようにすることができます。しかし、それらへのポインターがない場合、どのようにそれを行いますか?

個人的にshared_ptrスロットの有効期間を自動的に管理できる . 複数のスレッドがそれらのオブジェクトを混乱させたくない場合は、それらにアクセスを許可しないでください。それらのスレッドに共有ポインタを渡さないようにしてください。

しかし、私はさらに一歩進んでいきます。あなたのスロットが呼び出し可能オブジェクトであると思われる場合、私はshared_ptrまったくドロップせず、クラスstd::function<>内にそれらをカプセル化するために使用します。Signalつまり、シグナルが発信されるたびに呼び出されるオブジェクトを保持するだけvectorです。このようにして、コールバックを設定するためにstd::function<>単に継承するだけでなく、より多くのオプションを使用できます。単純な関数ポインター、または の結果、または思いつく任意のファンクター (ラムダも含む) を登録できます。Slotstd::bind

これで、Boost.Signals2 の設計に非常に似ていることがわかります。あなたの当初の設計目標がそれよりもスリムなものを作ることだったという事実を私が無視しているとは思わないでください。最先端のライブラリがそのように設計されている理由と、最終的にそれに頼ることが理にかなっている理由をお見せしたいと思います。

確かに、std::functionスマート ポインターではなくオブジェクトをSignalクラスに登録すると、ヒープに割り当てたファンクターの有効期間を気にする必要があります。ただし、それは必ずしもクラスの責任である必要はありません。Signalその目的のためにラッパー クラスを作成できます。これは、ヒープ上に作成したファンクターへの共有ポインター (から派生したクラスのインスタンスなど) を保持し、それらをオブジェクトSlotに登録できます。いくつかの適応により、これにより、「すべてかゼロか」ではなく、スロットを個別Signalに登録および切断することもできます。

答え:

しかし、あなたの要件が常に次のようになると仮定しましょう(後者の部分は本当に予測が難しいです)。

  1. 複数のシグナルに対して同じスロットを登録する必要はありません。
  2. 実行時にスロットの状態を変更する必要はありません。
  3. さまざまなタイプのコールバック (ラムダ、関数ポインター、ファンクターなど) を登録する必要はありません。
  4. 個々のスロットを選択的に切断する必要はありません。

次に、質問に対する回答を次に示します。

Q1: 「[...] Signal インスタンスと Slot インスタンスを次のように作成すると、引数が const ref として渡されることが保証されますか?」

A1: はい、転送パスに沿ったすべてが定数参照であるため、それらは定数参照として渡されます。

Q2: 「[Boost.Signals2 では] 任意のファンクターまたは関数ポインターを実際に接続できます。これは実際にどのように機能しますか?これは、boost::function を使用して任意の関数ポインターまたはメソッド ポインターをシグナルに接続する場合に役立つ可能性があります」

A2: これはboost::function<>クラス テンプレート (後std::functionに VS2010 でサポートされるようになり、私の記憶が正しければサポートされるはずです) に基づいており、型消去テクニックを使用して、型が異なるが同一のシグネチャの呼び出し可能なオブジェクトをラップします。実装の詳細に興味がある場合は、の実装boost::function<>参照するか、MS の実装を参照してくださいstd::function<>(非常に似ているはずです)。

これが少しお役に立てば幸いです。そうでない場合は、コメントで追加の質問をしてください。

于 2013-01-22T14:25:37.247 に答える