5

これは、質問のために大幅に簡略化されています。階層があるとしましょう:

struct Base {
    virtual int precision() const = 0;
};

template<int Precision>
struct Derived : public Base {

    typedef Traits<Precision>::Type Type;

    Derived(Type data) : value(data) {}
    virtual int precision() const { return Precision; }

    Type value;

};

署名付きの非テンプレート関数が必要です:

Base* function(const Base& a, const Base& b);

関数の結果の特定のタイプがどちらかと同じタイプでaありb、大きい方を持っているPrecision場合; 次の擬似コードのようなもの:

Base* function(const Base& a, const Base& b) {

    if (a.precision() > b.precision())

        return new A( ((A&)a).value + A(b.value).value );

    else if (a.precision() < b.precision())

        return new B( B(((A&)a).value).value + ((B&)b).value );

    else

        return new A( ((A&)a).value + ((A&)b).value );

}

ここAで、およびはそれぞれ、およびBの特定のタイプです。インスタンス化の数に関係なく動作したい。RTTIの回答は問題ありませんが、大量の比較表は避けたいと思います。何か案は?abfunctionDerivedtypeid()

4

3 に答える 3

4

あなたが求めているのは、マルチディスパッチ、別名マルチメソッドと呼ばれるものです。これはC++言語の機能ではありません。

特別な場合の回避策がありますが、自分でいくつかの実装を行うことを避けることはできません。

多重ディスパッチの一般的なパターンの1つは、「再ディスパッチ」、別名「再帰的遅延ディスパッチ」と呼ばれます。基本的に、1つの仮想メソッドは、すべてのパラメーターが解決されるまで、1つのパラメータータイプを解決してから、別の仮想メソッドを呼び出します。外部の関数(存在する場合)は、これらの仮想メソッドの最初のものを呼び出すだけです。

n個の派生クラスがあると仮定すると、最初のパラメーターを解決する1つのメソッド、2番目のパラメーターを解決するn、3番目のパラメーターを解決するn * nというようになります。最悪の場合、とにかく。これはかなりの手作業であり、typeidに基づく条件付きブロックを使用する方が初期開発では簡単かもしれませんが、再ディスパッチを使用する方が保守には堅牢です。

class Base;
class Derived1;
class Derived2;

class Base
{
  public:
    virtual void Handle (Base* p2);

    virtual void Handle (Derived1* p1);
    virtual void Handle (Derived2* p1);
};

class Derived1 : public Base
{
  public:
    void Handle (Base* p2);

    void Handle (Derived1* p1);
    void Handle (Derived2* p1);
};

void Derived1::Handle (Base* p2)
{
  p2->Handle (this);
}

void Derived1::Handle (Derived1* p1)
{
  //  p1 is Derived1*, this (p2) is Derived1*
}

void Derived1::Handle (Derived2* p1)
{
  //  p1 is Derived2*, this (p2) is Derived1*
}

//  etc

派生クラスのテンプレートを使用してこれを実装することは困難であり、それを処理するためのテンプレートメタプログラミングは、おそらく読み取り不可能で、保守不可能で、非常に壊れやすいでしょう。ただし、非テンプレートメソッドを使用してディスパッチを実装し、次にミックスインテンプレート(基本クラスをテンプレートパラメーターとして受け取るテンプレートクラス)を使用して、追加機能でそれを拡張することはそれほど悪くないかもしれません。

ビジターデザインパターンは、再ディスパッチIIRCと密接に関連しています(基本的にはそれを使用して実装されます)。

もう1つのアプローチは、問題を処理するように設計された言語を使用することです。C++でうまく機能するオプションがいくつかあります。1つは、 treeccを使用することです。これは、ASTノードと多重ディスパッチ操作を処理するためのドメイン固有言語であり、lexやyaccのように、出力として「ソースコード」を生成します。

ディスパッチの決定を処理するために行うのは、ASTノードIDに基づいてswitchステートメントを生成することだけです。これは、動的に型付けされた値クラスIDであるIYSWIMでも同じように簡単に実行できます。ただし、これらは、記述または保守する必要のないswitchステートメントであり、これが重要な違いです。私が抱えている最大の問題は、ASTノードのデストラクタ処理が改ざんされていることです。つまり、特別な努力をしない限り、メンバーデータのデストラクタは呼び出されません。つまり、フィールドのPODタイプで最適に機能します。

もう1つのオプションは、マルチメソッドをサポートする言語プリプロセッサを使用することです。Stroustrupが、ある時点でマルチメソッドをサポートするためのアイデアをかなりよく開発していたこともあり、これらのいくつかがありました。CMMはその1つです。Doublecppは別です。さらにもう1つはフロストプロジェクトです。CMMはStroustrupの説明に最も近いと思いますが、確認していません。

ただし、最終的には、多重ディスパッチは実行時の決定を行うための単なる方法であり、同じ決定を処理する方法は多数あります。スペシャリストDSLはかなりの面倒をもたらすので、通常、多数の多重ディスパッチが必要な場合にのみそれを行います。再ディスパッチとビジターパターンは堅牢なWRTメンテナンスですが、複雑さと煩雑さが犠牲になります。単純な場合には、単純な条件ステートメントの方が適している場合がありますが、コンパイル時に未処理の場合の可能性を検出することは、不可能ではないにしても難しいことに注意してください。

よくあることですが、少なくともC ++では、それを行う正しい方法は1つではありません。

于 2010-03-12T23:47:34.730 に答える
3

テンプレートはコンパイル時に展開され、コンパイル時にfunction()は実際にどの派生型になるかわからないため、可能なすべての型の大規模なリストから選択せずに、function()をテンプレートコードに直接委任することはできません。と呼ばれます。operation必要となるテンプレート関数のバージョンごとに、テンプレートコードのインスタンス化をコンパイルする必要があります。これは、無限のセットになる可能性があります。

そのロジックに従って、必要になる可能性のあるすべてのテンプレートを知っている唯一の場所は、Derivedクラス自体です。したがって、Derivedクラスには次のメンバーを含める必要があります。

Derived<Precision> *operation(Base& arg2) {
  Derived<Precision> *ptr = new Derived<Precision>;
  // ...
  return ptr;
}

function次に、そのように定義し、間接的にディスパッチを実行できます。

Base* function(const Base& a, const Base& b) {
  if (a.precision() > b.precision())
    return a.operation(b);
  else 
    return b.operation(a);
}

これは簡略化されたバージョンであることに注意してください。操作の引数が対称でない場合は、メンバー関数の2つのバージョンを定義する必要があります。1つthisは最初の引数の代わりに、もう1つは2番目の引数の代わりに使用します。

また、これは、の派生型を知らなくてもa.operation、の適切な形式を取得するための何らかの方法が必要であるという事実を無視しています。これは自分で解決する必要があります。実行時にディスパッチするため、のタイプをテンプレート化してこれを解決することは(以前と同じロジックで)不可能であることに注意してください。解決策は、取得したタイプと、そのオブジェクトの正確なタイプを知らなくても、高精度のタイプが同等以下の精度のオブジェクトから値を引き出す方法があるかどうかによって異なります。これは不可能な場合があります。その場合、タイプIDの一致の長いリストがあります。b.valuebbDerived

ただし、switchステートメントでこれを行う必要はありません。Derived各タイプに、より正確な関数にアップキャストするためのメンバー関数のセットを与えることができます。例えば:

template<int i>
upCast<Derived<i> >() {
  return /* upcasted value of this */
}

次に、operatorメンバー関数はを操作できb.upcast<typeof(this)>、必要な型の値を取得するために明示的にキャストを行う必要はありません。これらの関数をコンパイルするには、これらの関数の一部を明示的にインスタンス化する必要がある場合があります。確かに言うには、RTTIで十分な作業を行っていません。

ただし、基本的に問題は、N個の可能な精度がある場合、N N個の可能な組み合わせがあり、実際には、これらのそれぞれに個別にコンパイルされたコードが必要になるということです。の定義でテンプレートを使用できない場合は、これらの可能性functionのすべてのN Nのバージョンをコンパイルする必要があり、どういうわけかコンパイラにそれらすべてを生成するように指示する必要があり、どういうわけか正しいものを選択する必要があります実行時ににディスパッチします。メンバー関数を使用するトリックは、Nのこれらの要素の1つを取り除きますが、もう1つは残り、完全に汎用にする方法はありません。

于 2010-03-12T22:36:41.667 に答える
1

まず、precisionメンバーをstatic const int関数ではなく値にして、コンパイル時に操作できるようにします。ではDerived、次のようになります。

static const int precision = Precision;

次に、最も正確なBase/Derivedクラスを決定するためのヘルパー構造体が必要です。まず、ブール値に応じて2つのタイプのいずれかを選択するための一般的なhelper-helper構造体が必要です。

template<typename T1, typename T2, bool use_first>
struct pickType {
  typedef T2 type;
};

template<typename T1, typename T2>
struct pickType<T1, T2, true> {
  typedef T1 type;
};

次に、pickType<T1, T2, use_first>::typeはに解決され、それ以外のT1場合use_firstはに解決されます。したがって、これを使用して最も正確なタイプを選択します。trueT2

template<typename T1, typename T2>
struct mostPrecise{
  typedef pickType<T1, T2, (T1::precision > T2::precision)>::type type;
};

ここmostPrecise<T1, T2>::typeで、2つのタイプのどちらか大きい方のprecision値を示します。したがって、関数を次のように定義できます。

template<typename T1, typename T2>
mostPrecise<T1, T2>::type function(const T1& a, const T2& b) {
  // ...
}

そして、あなたはそれを持っています。

于 2010-03-12T22:13:36.933 に答える