2

純粋仮想メソッドを持つクラス Feature があります。

class Feature {
public:
  virtual ~Feature() {}
  virtual const float getValue(const vector<int>& v) const = 0;
};

このクラスは、FeatureA や FeatureB などのいくつかのクラスによって実装されます。別のクラス Computer (簡略化) は、getValue メソッドを使用して計算を行います。

class Computer {
public:
  const float compute(const vector<Feature*>& features, const vector<int>& v) {
    float res = 0;
    for (int i = 0; i < features.size(); ++i) {
      res += features[i]->getValue(v);
    }
    return res;
  }
};

今、私は FeatureC を実装したいと思っていますが、getValue メソッドに追加情報が必要であることに気付きました。FeatureC のメソッドは次のようになります

const float getValue(const vector<int>& v, const vector<int>& additionalInfo) const;

もちろん、Feature、FeatureA、FeatureB の getValue の署名を変更して、パラメーターとして additionalInfo を取得し、compute メソッドのパラメーターとして additionalInfo を追加することもできます。ただし、さらに追加情報が必要な FeatureD を実装したい場合は、これらすべての署名を後で再度変更する必要がある場合があります。これに対するよりエレガントな解決策があるかどうか、またはさらに読むために私に指摘できる既知の設計パターンがあるかどうか疑問に思います。

4

4 に答える 4

2

少なくとも 2 つのオプションがあります。

  1. 単一のベクトルをgetValue()に渡す代わりに、構造体を渡します。この構造体では、今日はベクトルを配置し、明日はさらにデータを配置できます。もちろん、プログラムの一部の具体的な実行で追加のフィールドが必要ない場合は、それらを計算する必要が無駄になる可能性があります。ただし、常にすべてのデータを計算する必要がある場合 (つまり、FeatureC が常に 1 つある場合) は、パフォーマンスが低下することはありません。
  2. getValue()必要なデータを取得するメソッドを持つオブジェクトへの参照に渡します。このオブジェクトは、コンピュータ自体、またはより単純なプロキシである可能性があります。次に、getValue()実装は必要なものを正確に要求でき、遅延計算できます。場合によっては、遅延によって無駄な計算が排除されますが、この方法で行う全体的な構造では、さまざまなデータを取得するために (おそらく仮想) 関数を呼び出さなければならないため、一定の小さなオーバーヘッドが発生します。
于 2013-07-04T10:55:38.720 に答える
1

この問題は、C++ コーディング標準 (Sutter、Alexandrescu) の項目 39 で対処されており、「仮想関数を非パブリックにすることを検討し、パブリック関数を非仮想にすることを検討してください」と題されています。

特に、Non-Virtual-Interface 設計パターンに従う動機の 1 つ (これが項目のすべてです) は次のように述べられています。

各インターフェイスは自然な形を取ることができます: パブリック インターフェイスをカスタマイズ インターフェイスから分離すると、それぞれが同じように見えるようにする妥協点を見つけようとするのではなく、自然に取りたい形を簡単にとることができます。多くの場合、2 つのインターフェイスは異なる数の関数や異なるパラメーターを必要とします。[...]

これは特に便利です

変更のコストが高い基本クラス

この場合に非常に役立つもう 1 つのデザイン パターンは、Visitor パターンです。NVI に関しては、基本クラス (および階層全体) の変更コストが高い場合に適用されます。この設計パターンについては、多くの議論があります。Modern C++ (Alexandrescu) の関連する章を読むことをお勧めします。これは、(非常に使いやすい) ビジター機能の使用方法に関する優れた洞察を提供します。ロキ

この資料をすべて読んでから、質問を編集して、より良い回答を提供できるようにすることをお勧めします. 私たちはあらゆる種類の解決策を考え出すことができます (たとえば、必要に応じて、クラスに追加のパラメーターを与える追加のメソッドを使用します)。

次の質問に答えてみてください。

  1. テンプレートベースのソリューションは問題に適合しますか?
  2. 関数を呼び出すときに間接的な新しいレイヤーを追加することは可能でしょうか?
  3. 「プッシュ引数」-「プッシュ引数」-...-「プッシュ引数」-「関数呼び出し」メソッドは役に立ちますか? (これは最初は非常に奇妙に思えるかもしれませんが、「cout << arg << arg << arg << endl」のように考えてください。ここで、「endl」は「呼び出し関数」です)
  4. Computer::compute で関数を呼び出す方法をどのように区別するつもりですか?

いくつかの「理論」が得られたので、ビジター パターンを使用した実践を目指しましょう。

#include <iostream>

using namespace std;

class FeatureA;
class FeatureB;

class Computer{
    public:
    int visitA(FeatureA& f);

    int visitB(FeatureB& f);
};

class Feature {
public:
  virtual ~Feature() {}
  virtual int accept(Computer&) = 0;
};

class FeatureA{
    public:
    int accept(Computer& c){
        return c.visitA(*this);
    }
    int compute(int a){
        return a+1;
    }
};

class FeatureB{
    public:
    int accept(Computer& c){
        return c.visitB(*this);
    }
    int compute(int a, int b){
        return a+b;
    }
};

int Computer::visitA(FeatureA& f){
        return f.compute(1);
}

int Computer::visitB(FeatureB& f){
        return f.compute(1, 2);
}

int main()
{
    FeatureA a;
    FeatureB b;
    Computer c;
    cout << a.accept(c) << '\t' << b.accept(c) << endl;
}

このコードはこちらで試すことができます。これは、ご覧のとおり、問題を解決する Visitor パターンの大まかな実装です。この方法で実装しようとしないことを強くお勧めします。非環式ビジターと呼ばれる改良によって解決できる明らかな依存関係の問題があります。Loki では実装済みですので、実装に悩む必要はありません。

実装とは別に、ご覧のとおり、型スイッチに依存しておらず (他の誰かが指摘したように、可能な限り避けるべきです)、クラスに特定のインターフェイス (たとえば、compute 関数の 1 つの引数) を持たせる必要はありません。 )。さらに、ビジター クラスが階層の場合 (この例では Computer を基本クラスにします)、この種の機能を追加するときに階層に新しい関数を追加する必要はありません。

visitA、visitB、...「パターン」が気に入らなくても、心配する必要はありません。これは単純な実装であり、必要ありません。基本的に、実際の実装では、visit 関数のテンプレートの特殊化を使用します。

これが役に立てば幸いです、私はそれに多くの努力をしました:)

于 2013-07-04T11:29:35.490 に答える
1

クラスに基づいて異なるメソッドを呼び出すようにフィーチャ クラス階層のユーザーに要求すると、ポリモーフィズムが無効になります。やり始めたらdynamic_cast<>()、デザインを再考する必要があることがわかります。

サブクラスがその呼び出し元からのみ取得できる情報を必要とする場合は、getValue() メソッドを変更して additionalInfo 引数を取得し、重要でないクラスではその情報を単純に無視する必要があります。

FeatureC が別のクラスまたは関数を呼び出して additionalInfo を取得できる場合、それについて知る必要があるクラスの数が制限されるため、通常はそれがより良いアプローチです。おそらく、データは、FeatureC がそのコンストラクターを介してアクセスできるオブジェクトから、またはシングルトン オブジェクトから利用できるか、関数を呼び出して計算できます。最善のアプローチを見つけるには、ケースについてもう少し知識が必要です。

于 2013-07-04T10:52:08.823 に答える
0

仮想関数が正しく機能するには、まったく同じ「シグネチャ」(同じパラメーターと同じ戻り値の型) が必要です。それ以外の場合は、「新しいメンバー関数」を取得するだけで、これは必要なものではありません。

ここでの本当の問題は、「呼び出し元のコードは、追加情報が必要であることをどのように認識するか」です。

const vector <int>& additionalInfoこれはいくつかの異なる方法で解決できます。最初の方法は、必要かどうかに関係なく、常に を渡すことです。

additionalInfoの場合を除いて何もないため、それが不可能な場合はFeatureC、「オプションの」パラメーターを使用vector<int>* additionalInfoできます。これは、値が利用できない場合は NULL であるベクトル ( ) へのポインターを使用することを意味します。

もちろんadditionalInfo、FeatureC クラスに格納できる値であれば、それも機能します。

もう 1 つのオプションは、基本クラスを拡張して、Featureさらに 2 つのオプションを持たせることです。

class Feature {
public:
  virtual ~Feature() {}
  virtual const float getValue(const vector<int>& v) const = 0;
  virtual const float getValue(const vector<int>& v, const vector<int>& additionalInfo) { return -1.0; };   
  virtual bool useAdditionalInfo() { return false; }
};

次に、ループを次のようにします。

for (int i = 0; i < features.size(); ++i) {
  if (features[i]->useAdditionalInfo())
  {
       res += features[i]->getValue(v, additionalInfo);
  }
  else
  {
     res += features[i]->getValue(v);
  }
}
于 2013-07-04T10:54:57.890 に答える