Edited at

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

More than 1 year has passed since last update.


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

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>
#include <type_traits>

template<class T>
class Interface; //prototype

template<class T, bool isExtended = std::is_base_of<Interface<T>, T>::value>
class Printer{
static_assert(isExtended, "T is not extended interface class");
};

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

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 : Interface<myclass3> {
};

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

int main(){

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

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

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

return 0;
}

2016/09/08 簡略化

Interfaceクラスを継承する際、動的で良いのなら純粋仮想関数を定義するところですが

ここでは、テンプレート引数でキャストしたthisポインタに定義したいインターフェース呼び出しをしています。

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

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

class myclass1 : Interface<myclass1>{

///

のように宣言します。

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

myclass3のようにインターフェースを定義していな関数はstatic_castでエラーを、

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

もしstd::is_base_ofの結果がtrueならば、特殊化テンプレート最優先の規則に従い、trueで特殊化されたPrinterが展開されます。

逆に、falseだった場合(継承していない場合)、static_assertで継承していない旨のエラーを通知します。

こんな感じでしょうか。

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

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

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


[6/15追記] 基底ポインタで複数の派生クラスインスタンスを扱うのと同じことを、静的に行う

なんか静的ポリモーフィズムの説明のためによくQiita内で引用されてるのを見て、ちょっと真面目に考えました。

こんな記事を参考資料にしていただいてありがとうございます。


#include <iostream>
#include <vector>
#include <type_traits>
#include <boost/variant.hpp>

template<class HEAD, class... Args>
struct head_type {
typedef HEAD type;
};

namespace detail {
template<template<class> class T, bool isEnd, class HEAD, class... Args>
struct is_all_base_of_impl;

template<template<class> class T, class HEAD, class... Args>
struct is_all_base_of_impl<T, false, HEAD, Args...> {
static constexpr bool value = std::is_base_of<T<HEAD>, HEAD>::value ? is_all_base_of_impl<T, sizeof...(Args) == 1, Args...>::value : std::false_type{};
};

template<template<class> class T, class HEAD, class... Args>
struct is_all_base_of_impl<T, true, HEAD, Args...> {
static constexpr bool value = std::is_base_of<T<HEAD>, HEAD>::value;
};
}

template<template<class> class T, class... Args>
struct is_all_same : std::conditional<sizeof...(Args) == 1,
std::is_base_of<T<typename head_type<Args...>::type>, typename head_type<Args...>::type>,
detail::is_all_base_of_impl<T, false, Args...>
>::type{};

template<class T>
class Interface; //prototype

template<class... Args>
class PrinterArrHelper {
public:
typedef boost::variant<Args...> TaskType;

PrinterArrHelper(Args&&... args) {
_tasks = std::vector<boost::variant<Args...>>{args...};
}

void printAll(){
for (auto && task : _tasks) {
PrintVisitor vis;
boost::apply_visitor(vis, task);
}
};

template<class T>
void addTask(T&& task) {
_tasks.push_back(TaskType{task});
}

private:
struct PrintVisitor {
using result_type = void;
template <class T>
void operator()(T& task) { task.print(); }
};

std::vector<boost::variant<Args...>> _tasks;
};

template<class... Args>
typename std::enable_if<is_all_same<Interface, typename std::remove_reference<Args>::type...>::value, PrinterArrHelper<Args...>>::type
createPrinterHelper(Args&&... args) { return PrinterArrHelper<Args...>(args...); }

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 {
public:
void print(){ std::cout << "myclass3" << std::endl; }
};

int main(){
myclass1 a;
myclass2 b;
myclass3 c;

auto helper = createPrinterHelper(a, b, b);
//auto helper = createPrinterHelper(a, b, c); // compile error
helper.printAll();

std::cout << "------" << std::endl;

helper.addTask(myclass2{});
helper.printAll();

return 0;
}

myclass1

myclass2
myclass2
------
myclass1
myclass2
myclass2
myclass2

https://wandbox.org/permlink/S9x1qJmvcegEjxCV

boost::variant使いました。

静的インターフェースの役割として、ちゃんとインターフェースクラスを継承しているかどうかをチェックしないといけないので、std::is_base_ofをVariadic Templatesに対応させました。

addTaskするときのインスタンスは最初にcreateHelperした時に使用した型のどれかに一致しないとダメなので、改善の余地があるな。

...安全で簡単な実装とは一体...これだからC++ユーザは...

では。