継承とテンプレート

この記事は,C++プログラマであるかを見分ける10の質問 - Life like a clown の「抽象クラスとテンプレートクラスの使い分けについてインターフェースと言う観点から述べよ」に対する回答的な記事です.

インターフェース的な基底クラスは,テンプレートで代替できる場合があります.例えば,

class interface {
public:
    virtual ~interface() {}
    virtual void do_something() = 0;
};

void func(interface& x) {
    x.do_something();
}

と言うインターフェース(的な)クラスを引数に取る関数 func() は,以下のように書く事でも実現できます.

template <class T>
void func(T& x) {
    x.do_something();
}

さて.この 2種類をどのように使い分けるかについては私自身もはっきりとした答えを持っていませんが,個人的には「可能ならばできるだけテンプレートの形で書く」と言う方針を取っています.その理由は,ユーザに class implemented : public interface と書かせるのがめんどくさいと言う切実な理由もありますが,その他にキャストは可能な限り発生させたくないと言うものがあります.virtual 関連の記事(overload と override と hiding - Life like a clown, virtual デストラクタ - Life like a clown)でも述べましたが,基底クラスのポインタから派生クラスのメンバ関数(メソッド)を実行する場合 virtual の有無で挙動が変わる事があり,プログラマの不注意などで予期せぬ不都合を発生させる事があります.そのため,アップキャストであっても発生させなくて良いのであればできるだけ発生させないと言うのが個人的な方針です.

継承とテンプレートの使用方法に関する別の比較例としては,インターフェース的なものを定義したときのその実装の仕方があります.例えば,上記の Interface を実装する場合,継承と言う手段を用いる場合は以下のようなコードになります.

class implemented : public interface {
public:
    virtual ~implemented() {}
    
    virtual void do_something() {
        std::cout << "implement do_something()" << std::endl;
    }
};

int main() {
    implemented x;
    x.do_something();
    return 0;
}

これをテンプレートを用いて代替する場合,以下のような書き方が考えられます.

class policy_implemented {
public:
    void exec() {
        std::cout << "implement do_something()" << std::endl;
    }
};

template <class DoSomethingPolicy>
class implemented {
    DoSomethingPolicy policy_;
public:
    explicit implemented(const DoSomethingPolicy& policy = DoSomethingPolicy()) :
        policy_(policy) {}
    
    void do_something() {
        policy_.exec();
    }
};

int main() {
    implemented<policy_implemented> x;
    x.do_something();
    return 0;
}

継承する代わりに,インターフェース的なクラス(のインスタンス)をメンバ変数に持たせて実際の処理をそのクラスに移譲すると言う戦略が考えられます.これは,デザインパターンで言うところの TemplateMethodStrategy の対比と考える事ができます.したがって,使い分けに関しても「TemplateMethod と Strategy をどう使い分けるか」と言う観点で考える事もできます.

継承とテンプレートの合わせ技

インターフェース的な使い方をする場合,継承とテンプレートとで代替可能な場面が多々あるのですが,中には(普通にやると)継承じゃないと(正確には,きちんとインターフェース的なクラスを定義していなければ)実現できないケースもあります.この際に,ユーザからこの継承部分を隠蔽する方法が提案されました.この方法は Type Erasure と呼ばれています.

非テンプレート基本クラスとテンプレート派生クラスによる Type Erasure では,その名のとおり,リスト 7-15 のような非テンプレート基本クラス (base) とテンプレート派生クラス (derived) を用意しておきます.

// リスト 7-15
class base {
public:
    virtual ~base() {}
};

template <class T>
class derived : public base {
    T value_;
public:
    derived(const T& value) : value_(value) {}
    virtual ~derived() {}
};

これを用意しておくと,継承の特性を利用して derived が持つ型情報 T を base に隠すことができ,メンバ関数テンプレートで受け取ったパラメータをメンバとして保持できます(リスト 7-16).

// リスト 7-16
class X {
    base* p_;
public:
    template <class T>
    X(const T& value) {
        p_ = new derived<T>(value);
    }
    
    ~X() { delete p_; }
};
C++テンプレートテクニック

Type Erasure については,静的型付け言語におけるダックタイピング (Type Erasure) - Life like a clownhttp://d.hatena.ne.jp/tt_clown/20101212/1292132302 辺りでも触れていますので,参照下さい.