C++ インターフェースの実現方法【インタフェースクラスとダックタイピング】

C++にはJava言語のインターフェース(interface)に相当する機能が存在しません。ただし、C++では多重継承や純粋仮想関数を用いることで、インターフェースの仕組みを実現することができます。C++の世界ではこれをインターフェースクラス(Interface Class)と呼びます。

他にもC++ではテンプレートの仕組みを用いることで、インターフェースの役割と目的の一部を果たすことが可能となっています。本記事ではC++でインターフェースを実現するための複数の方法を紹介していきます。

目次

# インターフェースクラスによる実現が一般的です。ダックタイピングを用いた方法は処理効率が求められるような環境では有効なテクニックですが、完全なポリモーフィズムの実現が困難だという問題があります。しかし、きちんとした使い分けを行えば、いずれも有効な技法となります。

インターフェースクラス

C++では純粋仮想関数仮想デストラクタを定義したクラスをインターフェースとして活用することができます。

インターフェースの定義

// インターフェースクラス
struct Animal {
   virtual void say() = 0; // 純粋仮想関数
   virtual ~Animal() {}    // 仮想デストラクタ
};

上記の例では、Animalがインターフェース(interface)に相当します。純粋仮想関数として宣言されたvoid say()は実装(implements)対象のメソッドとなります。

インターフェースの実装

インターフェースの実装は、一般的なクラスの継承を用いて行います。

struct Dog : Animal {
   void say() { puts("ワン"); } // オーバーライド
};

既に何らかの派生クラスとして継承済みのクラスの場合は、多重継承という形でインターフェースを実装します。

struct Cat : std::string, Animal {
   void say() { puts("にゃん"); }
}

override指定子

なおC++11ではoverride指定子の利用が可能となっています。これによりオーバーライド時の関数名や引数型に対する整合性が厳格にチェックされるようになります。

struct Animal { virtual void say() = 0; };

struct Cat : Animal {
   // 安全にオーバーライドされる
   void say() override { puts("にゃーん"); }
};

関数名や引数型の不一致、不用意なvirtual関数の定義を検知することができます。

struct Bird : Animal {
   // コンパイルエラー: Only virtual member functions can be marked 'override'
   void SAY() override { puts("ぴよぴよ"); }
   // コンパイルエラー: Non-virtual member function marked 'override' hides virtual member function
   void say(int arg) override { puts("ぴよぴよ"); }
   // コンパイルエラー: 'say' marked 'override' but does not override any member functions
   virtual void say(int arg) override { puts("ぴよぴよ"); }
};

インターフェースの利用

インターフェースクラスを実装した派生クラスは、インターフェースクラスへのアップキャストが可能となります。

Animal *dog = new Dog;
dog->say(); // "ワン"
Animal *cat = new Cat;
cat->say(); // "にゃん"

delete dog;
delete cat;

インターフェースクラスを実装した派生クラスは、多態性(ポリモーフィズム)を実現するため、上記の例ではアップキャスト前の型(Dog, Cat)のメンバ関数(say)がそれぞれ呼ばれることになります。

またdelete時には基底クラス側で定義していた仮想デストラクタの効果によって、派生クラス側で定義されたデストラクタが安全に呼び出されるようになります。

struct Bird : Animal {
   void say() { puts("ピヨピヨ"); }
   ~Bird()    { puts("じゃあね"); } // デストラクタ
};

Animal *bird = new Bird;
delete bird; // "じゃあね"
参考: 仮想デストラクタ - virtualデストラクタの目的や問題、回避方法
鳥「じゃあね」
猫「うわぁぁー!しゃべったぁー!!」

※ よく見たらオウムでした

インターフェースクラスの問題点

インターフェースクラスは仮想関数(純粋仮想関数や仮想デストラクタ)を用いているため、余計な仮想関数テープル(vtable)を生み出す原因となります。vtableはオブジェクトサイズの肥大化を招いたり、関数呼出し時のオーバヘッドを生み出す原因となります。また関数のインライン展開やコンパイラの最適化を阻害することにも繋がります。いずれも気にするほど大きな問題にはならないことがほとんどですが、クラスやプログラムの利用用途によっては問題になる場合があるため、注意事項として心に留めておいてください。

他にも多重継承によるダイアモンド継承(菱形継承)の問題にも注意する必要があります。インターフェースクラスを複数のクラスで多重継承するような形を取る場合や、インターフェースにデフォルトの実装を持たせるような設計をとる際には、仮想継承を用いて菱形継承問題へ対処する必要が出てきます。

テンプレートによるダックタイピング

C++のテンプレートは静的なダックタイピングを実現する用途で利用できます。

ダックタイピングとは

ダックタイピングとは、インターフェースの宣言や継承関係に依存しない型解決の特別な考え方です。あるオブジェクトからsizeというメソッドを呼び出すことができれば(object.size())、そのオブジェクトはsizeメソッドを実装しているとみなすことができます。

C++のテンプレートではこの「みなし」が可能となっており、事実、以下の例では基底クラスによる宣言なしに、メソッドの呼び出し処理を記述することが可能となっています。

template<class T> size_t get_size(T object) {
   return object.size();
}

上記のテンプレート関数get_sizeには、メンバ関数sizeを実装したオブジェクトのみを渡すことができます。

std::string s = "abc";
get_size(s); // OK

std::vector<int> v = {1, 2, 3};
get_size(v); // OK

メンバ関数sizeを実装していないオブジェクトを渡した場合には、コンパイルエラーが発生します。

struct X {} x;
get_size(x); // ERROR: No member named 'size' in 'X'

int i = 9;
get_size(i); // ERROR: Member reference base type 'int' is not a structure or union

このように、テンプレートを用いることで、基底クラスの宣言や定義を行う事無く、事実上のインターフェースの仕組みを実現することが可能となります。

ダックタイピングによるインターフェースの実現

先程の# インターフェースクラスの項で実現したインターフェースを、テンプレートを用いて実現することも可能です。

template<class Animal> void say(const Animal& animal) {
   animal.say();
}

struct Dog {
   void say() const { puts("ワン"); }
};
struct Cat {
   void say() const { puts("にゃん"); }
};
say(Dog()); // "ワン"
say(Cat()); // "にゃん"

基底クラスと派生クラスの継承関係も不要となります。そのためダックタイピングによるインターフェースは、様々なクラスや既存のクラスにも柔軟に適用することが可能となります。

静的ポリモーフィズムの問題点

ただし、インターフェースクラスの時のように、各クラスを基底クラスへアップキャストして利用するようなことができない点に注意が必要です。

各クラスをテンプレートクラスのメンバ変数として管理することは可能です。

template<class Animal> struct Cage {
   Animal animal;
   void say() { animal.say(); }
};

Cage<Dog> cage = {Dog()};
cage.say(); // "ワン"

これで静的なポリモーフィズムを実現することが可能になりました。ただし、テンプレートの性質上、動的なポリモーフィズムを実現することはできません。

// ERROR
std::vector<Cage> v = {Cage{Dog()}, Cage{Cat()}};
for (Cage cage : v) cage.say();
// OK: 本来の目的と異なる
std::vector<Cage<Dog>> v = {Cage<Dog>{Dog()}, Cage<Dog>{Dog()}};
for (Cage<Dog> cage : v) cage.say();

この場合、タプルを用いる等の解決策が考えられます。

auto tuple = std::make_tuple(Dog(), Cat());
std::get<0>(tuple).say(); // "ワン"
std::get<1>(tuple).say(); // "にゃん"

ただし、タプルのサイズが可変長になるような場合には、テンプレートメタプログラミングによる特殊なイディオム、またはapply関数(since C++17)の活用が求められます。

広告