BoostBoost.Signals 設計の論拠

本ドキュメントは、 Boost.Signals ライブラリに対してなされたいくつかの大きな設計上の決定に関して、 その背後にある論拠を解説する。

目次


スロットの定義の選択

スロットの定義は、シグナル・スロットライブラリによって異なる。 Boost.Signals では、スロットは非常に緩やかな方法で定義されている: それは、シグナルによって指定された型のパラメタを与えて呼び出すことが可能であり、 その戻り値がシグナルが想定する結果の型に変換可能であるような 任意の関数オブジェクトである。 しかしながら Boost.Signals を構築するに先立って考慮された別の定義は、 それに関連した利点と欠点を持つ。

スロットの定義に満足できないユーザは、 既定のスロット関数型を特定の用途にあった別のものに置き換えることが可能である。

ユーザレベルの接続管理

ユーザは、シグナルからスロットへの接続と来たるべき切断に関して、 洗練された制御を必要とする。 Boost.Signals が採用しているアプローチは、 接続状態問い合わせと手動での切断、ならびに破棄状態における自動切断を可能にする connection オブジェクトを返すことである。 他に見込みがあるインターフェースとして、次のようなものがある:

統合子インターフェース

統合子のインターフェースは、 C++ 標準ライブラリのアルゴリズム呼び出しに類似するように選択された。 スロット呼び出しの結果を入力イテレータによってアクセスされる単なる値のシーケンスのように見せることで、 統合子のインターフェースは熟達した C++ にとってもっとも自然なものとなっただろう。 競合するインターフェース設計では、 概して統合子を Signals ライブラリに特化した (そして限定された) インターフェースにしたがって構築する必要がある。 一般に、 このようなインターフェースはシグナル・スロットライブラリのより直裁的な実装を可能にする一方で、 統合子を他のシグナル・スロットライブラリはジェネリックアルゴリズムで再利用することは残念ながら不可能となり、 特定の統合子インターフェースを学ぶことが学習曲線をやや険しくする。

Signals における統合子の形式は、 統合子が通信に際して、 より複雑な "push" 方式ではなく "pull" 方式を利用することを基礎としている。 "pull" 機構では統合子の状態はスタックとプログラムカウンタ中に保持できる。 なぜなら新しいデータを必要とする (つまり次のスロットを呼び出して戻り値を受け取る) 際には、 いつでも統合子のコードから戻ることなしに即座にデータを受け取ることができる単純なインターフェースが存在するからだ。 これは、 シグナル呼び出しの度に統合子の手続きが呼び出されるため、 統合子が全状態をクラスメンバに保持しなければならない "push" 機構と対照的だ。 例として、 スロット呼び出しの最大要素を戻す統合子を比較してみる。 もし最大要素 100 を超えたら、それ以上のスロットは呼び出さないものとする。
Pull Push
struct pull_max {
  typedef int result_type;

  template<typename InputIterator>
  result_type operator()(InputIterator first,
                         InputIterator last)
  {
    if (first == last)
      throw std::runtime_error("Empty!");

    int max_value = *first++;
    while(first != last && *first <= 100) {
      if (*first > max_value)
        max_value = *first;
      ++first;
    }

    return max_value;
  }
};
  
struct push_max {
  typedef int result_type;

  push_max() : max_value(), got_first(false) {}

  // returns false when we want to stop
  bool operator()(int result) {
    if (result > 100)
      return false;

    if (!got_first) {
      got_first = true;
      max_value = result;
      return true;
    }

    if (result > max_value)
      max_value = result;

    return true;
  }

  int get_value() const 
  { 
    if (!got_first)
      throw std::runtime_error("Empty!");
    return max_value; 
  }

private:
  int  max_value; 
  bool got_first;
};
  

これらの例において注意すべき点がいくつかある。 "pull" 版は、 value_type が汎整数型であるような入力イテレータシーケンスに基づいた再利用可能な関数オブジェクトであり、 意図も非常に直裁的である。 一方 "push" 方式は呼び出し側の特定のインターフェースに依拠しており、 たいていは再利用不可能である。 また決定に際して余分な状態値、 たとえば要素を一つでも受け取ったか、 を必要とする。 一般にコードの品質と利用しやすさは主観的なものだが、 明らかに "pull" 方式は短く、再利用性に富み、 たいていはシグナル・スロットライブラリの文脈外であっても、 書き、理解するのが容易である。

"pull" 統合子インターフェースのコストは Signals ライブラリ自身の実装において支払われている。 呼び出し中 (例: 参照外し演算子実行中) のスロット切断を正しく扱うために、 切断されたスロットを飛ばすイテレータを構築しなければならない。 加えてイテレータはそれぞれのスロットに渡す実引数の集合を持ち運ぶ必要があり (これらの実引数を格納した構造体への参照で十分であるが)、 複数回の参照外しが複数回のスロット呼び出しとならないよう、 スロット呼び出しの結果をキャッシュしなければならない。 これは明らかに大きなオーバーヘッドを必要とするが、 スロット呼び出しの全過程を考えると、 このオーバーヘッドは "push" 方式におけるオーバーヘッドとほぼ等価であると考えられる。 我々は統合子の状態検出を複雑にする代わりに、 イテレーションと参照外しを行う制御構造が複雑になるように逆転させたのである。

接続インターフェース: += 演算子

Boost.Signals は、 接続に関して sig.connect(slot) 形式の構文をサポートしている。 しがしながら、より簡潔な構文である (そして他のシグナル・スロット実装で用いられている) sig += slot が提案されたことがある。 この構文が却下された理由は、いくつかある:

trackable の論拠

trackable クラスは自動的な接続寿命管理に関する主要なユーザインターフェースであり、 その設計はユーザに直接的に影響を及ぼす。 二つの点がもっとも目立っている: それは trackable をコピーする際の奇妙な振る舞いと、 そして自動切断管理に関係する型を作成するには trackable から派生することを要求するという制約である。

trackable コピー時の振る舞い

trackable のコピー時の振る舞いは、 本質的に trackable 部分オブジェクトは決してコピーされないということである; コピー操作はほとんど何も行わない。 これを理解するためにシグナル・スロット接続の性質を調べ、 接続が接続状態にある実体を基礎としていることに注目しよう; 実体が破棄されると接続も破棄される。 したがって trackable 部分オブジェクトがコピーされると、 接続をコピーすることが不可能になる。 なぜなら接続は目標となる実体を参照しているのではなく、 その源となる実体を参照しているからだ。 この理由はシグナルがコピー不可能である理由と対をなしている: シグナルに接続されたスロットは特定のシグナルに接続されているのであって、 シグナル中のデータに接続されているのではない。

trackable から派生させる理由

trackable た正しく働くために二つの制約が存在する:

明らかに trackable から派生させることはこれらの二つの指針を満足する。 我々は、これに勝る解決策を発見していない。

libsigc++

libsigc++ は、 当初は C++ で GTK の C 言語インターフェースをラップしようという提唱の一部として開始され、 Karl Nelson によって保守される別個のライブラリに成長した C++ のシグナル・スロットライブラリである。 libsigc++ と Boost.Signals には多くの類似点があり、 実際のところ Boost.Signals は Karl Nelson と libsigc++ に強く影響されている。 それぞれのライブラリを大雑把に調査すると、 シグナル構築や接続の利用法、 自動的な接続寿命管理に関して類似した構文を見つけるだろう。 これらのライブラリを区別する、設計上の大きな差異もいくつか存在する。

.NET デリゲート

Microsoft は .NET フレームワークと、 それに関連した一連の言語、言語拡張を登場させたが、 そのうちの一つがデリゲートである。 デリゲートはシグナルとスロットに類似しているが、 ほとんどの C++ のシグナル・スロットの実装と比較して限定されたものとなっている。 デリゲートは


Doug Gregor
Last modified: Fri Oct 11 05:41:04 EDT 2002