世界の隅の開発室

 

◎  スポンサーサイト 

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

◎  静的ポリモーフィズムの安全で簡単な実装 -動的から静的にしてパフォーマンス向上- 

動的ポリモーフィズムの確認



C++はオブジェクト指向言語です。
当然、オブジェクト指向の要素「ポリモーフィズム」を、言語仕様としてサポートしています。

C++では一般にポリモーフィズムは「動的ポリモーフィズム」を指し、実行時に基底クラスから派生クラスへと振る舞いを変化させます。


#include<iostream>

class Base{
public:
virtual void print(){ std::cout << \"Base\" << std::endl; }
virtual ~Base()=default;
};

class Super : public Base{
public:
void print() override{ std::cout << \"Super\" << std::endl; }
};

int main(){
Base* array[2] = {new Base,new Super} ;
array[0]->print();
array[1]->print();
delete array[0]; delete array[1];
return 0;
}


このプログラムを実行すると
Base
Super
と出力されます。

Baseポインタで管理されているインスタンスですが、片方の実体はその継承クラスSuperであるため、virtualで定義された仮想関数の機能によりSuperのprint()が実行されます。

このように、Base*ポインタとして扱われているインスタンスが、実際の仮想関数呼び出し時に本来の姿を取り戻すこの性質を動的ポリモーフィズムと呼びます。
動的ポリモーフィズムのデメリット

このように、とても強力な動的ポリモーフィズムですが、欠点も多くあります。

代表的なものとして、スタックの圧迫、関数呼び出しのオーバーヘッドの増加、インライン展開が不可能、などが挙げられます。

というのも、C++がこの動的ポリモーフィズムを実装するために使っているvtableが原因なのです。
これは仮想関数のポインタ解決を行うためのテーブルで、各インスタンスはそのテーブルを保持し、そのテーブルから仮想関数の呼び出しをする機構になっています。

各インスタンスが関数ポインタテーブルを持つことでスタックを圧迫し、
関数ポインタテーブルで関数を間接参照することでオーバーヘッドが生じ、
終いの果てには実行時まで呼び出す関数が分からないためにインライン展開ができなくなる、というパフォーマンスに関する多くのディスアドバンテージを抱えてしまいます。

これを解決するために、多くのC++ユーザは動的ではない静的なポリモーフィズムでの実装を推奨しています。
つまり、コンパイル時に呼び出す関数を解決するのです。

まぁ、テンプレートを使うんですけど。
静的ポリモーフィズムの実装

継承ではなくテンプレートを使うだけです。


#include<iostream>

class myclass1{
public:
void print(){ std::cout << \"myclass1\" << std::endl; }
};

class myclass2{
public:
void print(){ std::cout << \"myclass2\" << std::endl; }
};

template<class T>
class Printer{
T obj;
public:
void print(){
obj.print();
}
};

int main(){
Printer<myclass1> a;
Printer<myclass2> b;

a.print();
b.print();

return 0;
}


非常に簡単ですが、色々と問題があります。

まず、動的ならばできるはずの、一つの配列で複数の種類のインスタンスを管理することができません。
テンプレートはクラスを自動で展開しているだけなので、Printer<myclass1>とPrinter<myclass2>は完全に別のクラスですから。

また、このままだとテンプレート引数は何を渡していいのか分かりません。
こういった場合変なクラスを渡してしまい読み辛いエラーが返ってきたりします。

継承をうまく使って「インターフェースの共通化」を行い、ついでにちょこっとメタなことをして「変なクラスを弾く」ようにしてみましょう。


#include<iostream>

template<class T>
class _Printer; //prototype

template<class T>
class Interface; //prototype

template<bool is_base,class T>
struct Dummy;

template<class T>
struct Dummy<true,T>{ // T is extends Interface<T>
using type = _Printer<T>;
};

template<class T>
using Printer = typename Dummy<std::is_base_of<Interface<T>,T>::value,T>::type;

template<class T>
class Interface{
public:
void print(){ static_cast<T &>(this)->print(); }
};

class myclass1 : Interface<myclass1>{
public:
void print(){ std::cout << \"myclass1\" << std::endl; }
};

class myclass2 : Interface<myclass2>{
public:
void print(){ std::cout << \"myclass2\" << std::endl; }
};

class myclass3{
void print(){ std::cout << \"myclass3\" << std::endl; }
};

template<class T>
class _Printer{
T _obj;
public:
void print(){ _obj.print(); };
};

int main(){

Printer<myclass1> a;
Printer<myclass2> b;

//Printer<myclass3> c; //compile error

a.print();
b.print();

return 0;
}


Interfaceクラスを継承する際、動的で良いのなら純粋仮想関数を定義するところですが
ここでは、テンプレート引数でキャストしたthisポインタに定義したいインターフェース呼び出しをしています。

これによりインターフェースクラスとしての性質を持つことができます。

このインターフェースクラスのテンプレート引数は、継承したクラス自身の型で、コードを見れば分かりますが

class myclass1 : Interface<myclass1>{
///

のように宣言します。

そして、myclass1と2を静的ポリモーフィズムで扱うことができます。

また、myclass3のようなインターフェースは同一でもInterfaceクラスを継承していない不正なクラスを弾くために、メタ関数とstd::is_base_ofを使っています。

テンプレートエイリアスをかけてDummyメタ関数をクッションするようにし、テンプレート引数Tの型がInterfaceを継承しているかどうかを検査します。

もしstd::is_base_ofの結果がtrueならば、特殊化テンプレート最優先の規則に従い、trueで特殊化されたDUMMYが展開されます。
逆に、falseだった場合(継承していない場合)、未定義のテンプレートクラスを展開しようとするため、エラーを吐きます。

Dummyが無事展開されれば、Dummyは隠蔽した_Printerクラスをtypedefし、無事テンプレートエイリアスのPrinterは目的のクラスのシンボルとなります。

こんな感じでしょうか。

全体的に、テンプレートに理解があればそんなに難しくはないと思いますが、一方その恩恵はかなり大きいので、もし動的から静的へ置換可能である場合には是非試してみると良いと思います。

残念ながら、動的ポリモーフィズムの特徴の一つ、基底ポインタで複数の派生クラスインスタンスを扱うのと同じことを、静的に行う方法は分かりませんでした。

多分無理かな? わかりません、C++は奥が深いですからね。

では。
スポンサーサイト

COMMENT FORM

  • URL:
  • comment:
  • password:

Trackback

トラックバックURL:http://joints.blog111.fc2.com/tb.php/36-6116e17e


back to TOP

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。