web-dev-qa-db-ja.com

テンプレートまたは抽象基本クラス?

クラスを適応可能にし、外部からさまざまなアルゴリズムを選択できるようにしたい場合、C++での最適な実装は何ですか?

私は主に2つの可能性を見ています:

  • 抽象基本クラスを使用して、具体的なオブジェクトを
  • テンプレートを使用する

これは、さまざまなバージョンで実装された小さな例です。

バージョン1:抽象基本クラス

class Brake {
public: virtual void stopCar() = 0;  
};

class BrakeWithABS : public Brake {
public: void stopCar() { ... }
};

class Car {
  Brake* _brake;
public:
  Car(Brake* brake) : _brake(brake) { brake->stopCar(); }
};

バージョン2a:テンプレート

template<class Brake>
class Car {
  Brake brake;
public:
  Car(){ brake.stopCar(); }
};

バージョン2b:テンプレートとプライベート継承

template<class Brake>
class Car : private Brake {
  using Brake::stopCar;
public:
  Car(){ stopCar(); }
};

Javaから来た私は、当然、常にバージョン1を使用する傾向がありますが、テンプレートバージョンが頻繁に好まれるようです。 STLコードで?それが本当なら、それは単にメモリ効率など(継承なし、仮想関数呼び出しなし)のためですか?

バージョン2aと2bの間に大きな違いはないことに気づきました。 C++ FAQ を参照してください。

これらの可能性についてコメントできますか?

39
Frank

これはあなたの目標に依存します。次の場合はバージョン1を使用できます

  • 車のブレーキを交換するつもり(実行時)
  • 車をテンプレート以外の関数に渡すつもり

ランタイムポリモーフィズムを使用するバージョン1の方が一般的に好まれます。これは、バージョン1がまだ柔軟性があり、車に同じタイプを持たせることができるためです。Car<Opel>Car<Nissan>とは別のタイプです。ブレーキを頻繁に使用しながら優れたパフォーマンスを目標とする場合は、テンプレート化されたアプローチを使用することをお勧めします。ちなみに、これはポリシーベースの設計と呼ばれます。 ブレーキポリシーを提供します。たとえば、Javaでプログラミングしたと言ったので、おそらくC++の経験はまだあまりありません。それを行う1つの方法:

template<typename Accelerator, typename Brakes>
class Car {
    Accelerator accelerator;
    Brakes brakes;

public:
    void brake() {
        brakes.brake();
    }
}

ポリシーがたくさんある場合は、それらを独自の構造体にグループ化して、たとえばSpeedConfiguration収集AcceleratorBrakesなどとして渡すことができます。私のプロジェクトでは、大量のコードテンプレートを使用せずに、ヘッダー内のコードを必要とせずに、コードを独自のオブジェクトファイルに一度コンパイルできるようにしていますが、(仮想関数を介して)ポリモーフィズムを許可しています。たとえば、テンプレート以外のコードが多くの場合に呼び出す可能性のある共通のデータと関数を基本クラスに保持したい場合があります。

class VehicleBase {
protected:
    std::string model;
    std::string manufacturer;
    // ...

public:
    ~VehicleBase() { }
    virtual bool checkHealth() = 0;
};


template<typename Accelerator, typename Breaks>
class Car : public VehicleBase {
    Accelerator accelerator;
    Breaks breaks;
    // ...

    virtual bool checkHealth() { ... }
};

ちなみに、これはC++ストリームが使用するアプローチでもあります。std::ios_baseには、文字の型やopenmode、フォーマットフラグなどの特性に依存しないフラグなどが含まれ、std::basic_iosはクラステンプレートです。それはそれを継承します。これはまた、クラステンプレートのすべてのインスタンス化に共通のコードを共有することにより、コードの膨張を減らします。

プライベート継承?

一般に、私的継承は避けるべきです。それが役立つことはめったになく、ほとんどの場合、封じ込めの方が良い考えです。サイズが非常に重要な場合(たとえば、ポリシーベースの文字列クラス)に逆のことが当てはまる一般的なケース:空のポリシークラス(関数のみを含む)から派生する場合は、空の基本クラスの最適化を適用できます。

読む 継承の使用と乱用 ハーブサッターによる。

経験則は次のとおりです。

1)コンパイル時に具象タイプを選択する場合は、テンプレートを選択します。より安全になり(コンパイル時エラーと実行時エラー)、おそらくより適切に最適化されます。 2)選択が実行時に行われる場合(つまり、ユーザーのアクションの結果として)、実際には選択の余地はありません-継承と仮想関数を使用してください。

32

別のオプション:

  1. Visitor Pattern を使用します(外部コードをクラスで機能させます)。
  2. クラスの一部を、たとえばイテレータを介して外部化し、一般的なイテレータベースのコードがそれらを処理できるようにします。これは、オブジェクトが他のオブジェクトのコンテナである場合に最適に機能します。
  3. 戦略パターン も参照してください(内部にはc ++の例があります)
6
Assaf Lavie

テンプレートは、型をあまり気にしない変数をクラスで使用できるようにする方法です。継承は、クラスがその属性に基づいて何であるかを定義する方法です。その "is-a"対 "has-a" 質問。

5
John Ellinwood

あなたの質問のほとんどはすでに答えられていますが、私はこのビットについて詳しく説明したいと思いました:

Javaから来た私は、当然、常にバージョン1を使用する傾向がありますが、テンプレートバージョンが頻繁に好まれるようです。 STLコードで?それが本当なら、それは単にメモリ効率など(継承なし、仮想関数呼び出しなし)のためですか?

それはその一部です。しかし、別の要因は、追加された型安全性です。 BrakeWithABSBrakeとして扱うと、型情報が失われます。オブジェクトが実際にBrakeWithABSであることはもうわかりません。テンプレートパラメータの場合は、正確な型を使用できます。これにより、コンパイラがより適切な型チェックを実行できる場合があります。または、関数の正しいオーバーロードが呼び出されるようにするのに役立つ場合があります。 (stopCar()がBrakeオブジェクトを2番目の関数に渡す場合。2番目の関数はBrakeWithABSに対して個別のオーバーロードを持っている可能性があり、継承を使用した場合は呼び出されません。BrakeWithABSBrakeにキャストされていました。

もう1つの要因は、柔軟性が向上することです。すべてのBrake実装が同じ基本クラスから継承する必要があるのはなぜですか?基本クラスには実際にテーブルに持ってくるものがありますか?期待されるメンバー関数を公開するクラスを作成する場合、ブレーキとして機能するのに十分ではありませんか?多くの場合、インターフェイスまたは抽象基本クラスを明示的に使用すると、必要以上にコードが制約されます。

(注:テンプレートが常に推奨されるソリューションであるとは限りません。コンパイル速度から「チームのプログラマーが精通していること」または単に「私が好むこと」まで、これに影響を与える可能性のある他の懸念があります。 、あなた必要ランタイムポリモーフィズム。この場合、テンプレートソリューションは単純に不可能です)

4
jalf

この答え は多かれ少なかれ正しいです。コンパイル時にパラメータ化されたものが必要な場合は、テンプレートを選択する必要があります。実行時にパラメータ化されたものが必要な場合は、仮想関数をオーバーライドすることをお勧めします。

ただし、テンプレートを使用しても、両方を実行できなくなるわけではありません(テンプレートバージョンをより柔軟にします)。

struct Brake {
    virtual void stopCar() = 0;
};

struct BrakeChooser {
    BrakeChooser(Brake *brake) : brake(brake) {}
    void stopCar() { brake->stopCar(); }

    Brake *brake;
};

template<class Brake>
struct Car
{
    Car(Brake brake = Brake()) : brake(brake) {}
    void slamTheBrakePedal() { brake.stopCar(); }

    Brake brake;
};


// instantiation
Car<BrakeChooser> car(BrakeChooser(new AntiLockBrakes()));

そうは言っても、私はおそらくこれにテンプレートを使用しないでしょう...しかし、それは本当に個人的な好みです。

3
Greg Rogers

抽象基本クラスには仮想呼び出しのオーバーヘッドがありますが、すべての派生クラスが実際には基本クラスであるという利点があります。テンプレートを使用する場合はそうではありません– Car <Brake>とCar <BrakeWithABS>は互いに無関係であり、dynamic_castしてnullをチェックするか、Carを処理するすべてのコードのテンプレートを用意する必要があります。

2
sharptooth

個人的には、いくつかの理由から、テンプレートよりもインターフェイスを使用することを常に好みます。

  1. テンプレートのコンパイルとリンクのエラーは時々不可解です
  2. テンプレートに基づくコードをデバッグするのは困難です(少なくともVisual Studio IDEでは)
  3. テンプレートを使用すると、バイナリを大きくすることができます。
  4. テンプレートでは、すべてのコードをヘッダーファイルに入れる必要があります。これにより、テンプレートクラスが少しわかりにくくなります。
  5. テンプレートは、初心者のプログラマーが保守するのは困難です。

仮想テーブルが何らかのオーバーヘッドを作成する場合にのみテンプレートを使用します。

もちろん、これは私の自己意見にすぎません。

0
user88637

異なるBreakクラスとその階層を一度にサポートする場合は、インターフェイスを使用してください。

Car( new Brake() )
Car( new BrakeABC() )
Car( new CoolBrake() )

そして、あなたはコンパイル時にこの情報を知りません。

2bを使用するブレークがわかっている場合は、さまざまな車のクラスを指定するのが適切です。この場合のブレーキはあなたの車の「戦略」になり、デフォルトのものを設定することができます。

2aは使いません。代わりに、静的メソッドをBreakに追加して、インスタンスなしで呼び出すことができます。

0
Mykola Golubyev