こんにちは、トイロジックでグラフィックス関連を担当しているJKです。この記事では、開発プラットフォームの違いによる環境依存コードの抽象化する手法の1つをご紹介します。まずはポリモーフィズムについて少しみていきましょう。
動的ポリモーフィズムについて
C++ではオブジェクト毎にクラスの継承を用いて実行時に基底クラスから派生クラスへと同じシグネチャの仮想関数の振る舞いを変化させることで実現できます。これは型を扱う際の柔軟性が高いというメリットでもあります。また代表的なデメリットにインライン展開不可、関数呼び出しのオーバーヘッド、スタック領域の圧迫などパフォーマンスに影響を与えるものがあります。
静的ポリモーフィズムについて
動的ポリモーフィズム
が継承を使用したのに対して静的ポリモーフィズム
ではテンプレートを使用します。クラスが満たすべき特徴をコンパイル時に決定するという意味で静的ということになります。またコンパイル時に型が決定するというのはパフォーマンスの観点ではメリットとなり、型の柔軟性という観点ではデメリットとも言えると思います。
今回は、主にTraits
という手法を用いて抽象化に関する解説をしていきます。
Traitsとは
C++のテンプレートの特殊化を用いて、テンプレートクラス
や関数の定義
や処理内容
をその型ごとに変更できます。これを利用して共通コードからみた型ごとのシグネチャの差異をなくして、それらに依存せず呼び出せるようにする手法です。
Traitsを使用した環境依存コードの抽象化の例
この例ではプラットフォームごとに実装された環境依存のレンダラを例に考えてみましょう。
#include <iostream>
// 各環境依存のレンダラの実装部分.
class renderer_pc
{
public:
auto render_pc() -> void const
{
std::cout << "render for pc\n";
}
};
class renderer_ps5
{
public:
auto render_ps5() -> void
{
std::cout << "render for ps5\n";
}
};
class renderer_switch
{
public:
auto render_switch() -> void
{
std::cout << "render for switch\n";
}
};
上記がプラットフォームごとの環境依存したレンダラの例です。このままではメンバ関数のシグネチャが一致せずに共通コードで呼び出せないですね。
#include <type_traits>
// traitsによる特殊化.
template <class T>
struct renderer_traits : public std::bool_constant<false>{};
template <>
struct renderer_traits<renderer_pc> : public std::bool_constant<true>
{
static auto render(renderer_pc& r) -> void
{
r.render_pc();
}
};
template <>
struct renderer_traits<renderer_ps5> : public std::bool_constant<true>
{
static auto render(renderer_ps5& r) -> void
{
r.render_ps5();
}
};
template <>
struct renderer_traits<renderer_switch> : public std::bool_constant<true>
{
static auto render(renderer_switch& r) -> void
{
r.render_switch();
}
};
そこで上記のように各環境依存のレンダラ型ごとにテンプレートクラス特殊化します。ここでは同じような振る舞いをさせる為にrender()関数
の第一引数の型以外のシグネチャを揃えています。
template <class T>
auto render(T &renderer) -> void
{
renderer_traits<T>::render(renderer);
}
これで上記のように同じ振る舞いで呼び出しができるようになりました。ですがこのままでは共通コード部分に環境依存のレンダラの型を引っ張ってくる必要があり、環境依存とそうでないコードが混在していてあまりよくありませんね。Traits(静的ポリモーフィズム)
は性質上どうしても型の柔軟性が低くなってしまう為、適材適所で動的ポリモーフィズムやそれに近い手法の使い分けが必要な場合もあります。
// 型を持った派生クラスを、型を持たない基底クラスで捉える型消去(Type Erasure)の手法の1つ.
class renderer_holder_base
{
public:
virtual ~renderer_holder_base() {};
virtual auto render() -> void = 0;
};
template <class T>
class renderer_holder : public renderer_holder_base
{
static_assert(renderer_traits<T>::value, "unsupported renderer.");
public:
renderer_holder(T const& renderer)
: renderer_(renderer) {}
auto render() -> void override
{
renderer_traits<T>::render(renderer_);
}
private:
T renderer_;
};
class renderer
{
public:
template <class T>
renderer(T const& v)
:holder_base_(new renderer_holder<T>(v)) {}
~renderer()
{
if (holder_base_)
{
delete holder_base_;
holder_base_ = nullptr;
}
}
auto render() -> void
{
if (holder_base_)
{
holder_base_->render();
}
}
private:
renderer_holder_base *holder_base_ = nullptr;
};
ここでは環境依存コードのレンダラクラスを型消去(Type Erasure)手法
の1つを用いて隠蔽しています。これは型を持った派生クラスを、型を持たない基底クラスで捉える手法です.
// 環境に依存しない共通の描画呼び出し部分を想定.
auto scene_render(renderer r) -> void
{
r.render();
}
int main()
{
{
auto renderer_impl = renderer_pc{};
scene_render(renderer_impl);
}
{
auto renderer_impl = renderer_ps5{};
scene_render(renderer_impl);
}
{
auto renderer_impl = renderer_switch{};
scene_render(renderer_impl);
}
return 0;
}
これで共通コード側から型情報を隠蔽しつつ環境依存コードを呼び出せるようになりました。
最後に
いかがでしたでしょうか。今回はTraits
を用いて開発プラットフォームの違いによる環境依存コードの抽象化をご紹介させていただきました。またTraits
以外にもTag Dispatch
などの手法もありますので、次の機会があればご紹介したいと思います。