オブジェクト毎に内部インターフェースをカスタマイズする方法
複数のオブジェクトクラスに対して、同じ動作をさせるクラスの設計をすることがある。
例えば、ファイルへのIO処理はいろいろなオブジェクトを文字列かバイナリに相互変換させるクラスあるいは関数を設計する。
毎度個別に記述するのも良いが、もう少し簡便になるようにしたいことも多い。
どちらを選ぶかは、ケースバイケースだと思う。
さて、そのような汎用クラスの設計でC++言語ならテンプレートを用いることができる。ラムダ式? 自分にはよく分からないのでパス。
以下のテンプレート宣言は、「bool g(T a)」の部分をオブジェクト(T)毎に最適化することを想定した。
template <typename T> class A1 { private: bool g( T a ) { return true; } public: A1() {} A1( const A1& lhs ) = default; A1( A1&& rhs ) = default; A1& operator=( const A1& lhs ) = default; A1& operator=( A1&& rhs ) = default; virtual ~A1() = default; bool f( T a ) { return g(a); } };
例えばオブジェクトをintにするときには、以下のように追加で記述する。
template<> class A1<int> { private: bool g( int n ) { return ( 0 == n ); } public: A1<int>() {} A1<int>( const A1<int>& lhs ) = default; A1<int>( A1<int>&& rhs ) = default; A1<int>& operator=( const A1<int>& lhs ) = default; A1<int>& operator=( A1<int>&& rhs ) = default; virtual ~A1<int>() = default; bool f( int a ) { return g(a); } };
利用側では、こんな感じ
A1<int> a1; bool b0 = a1.f( 0 ); // true bool b1 = a1.f( 1 ); // false
この実装では特化テンプレートを定義することで行っているが、この場合オブジェクト毎に全て書き直しなのであまりうれしくない。
オブジェクトに関係しない部分をベースクラスにしてpublic継承もできるが、オブジェクト毎にテンプレート化する部分は毎度書くのでメンテナンス性も良くない。
では、派生クラスで実装してみるのはどうだろうか。
オブジェクト毎に特有の部分を仮想関数で実装する。
template <typename T> class A2 { private: virtual bool g( T a ) = 0; public: A2() {} A2( const A2& lhs ) = default; A2( A2&& rhs ) = default; A2& operator=( const A2& lhs ) = default; A2& operator=( A2&& rhs ) = default; virtual ~A2() = default; bool f( T a ) { return g(a); } };
オブジェクトをintにするときには、以下のように追加で記述する。
class B2 : public A2<int> { private: virtual bool g( int n ) override { return ( 0 == n ); } public: B2() : A2<int>() {} B2( const B2& lhs ) = default; B2( B2&& rhs ) = default; B2& operator=( const B2& lhs ) = default; B2& operator=( B2&& rhs ) = default; virtual ~B2() = default; };
利用側では、こんな感じ
B2 b2; bool b0 = b2.f( 0 ); // true bool b1 = b2.f( 1 ); // false
記述は派生クラスの実装で最低限必要な分だけになった。呼び出しは仮想関数の分だけ少しオーバーヘッドがある。
特化する部分を追い出したテンプレートにする方法もある。
template <typename T, class C> class A3 { private: bool g( T a ) { return C::f( a ); } public: A3() {} A3( const A3& lhs ) = default; A3( A3&& rhs ) = default; A3& operator=( const A3& lhs ) = default; A3& operator=( A3&& rhs ) = default; ~A3() = default; bool f( T a ) { return g(a); } };
オブジェクトをintにするときには、以下のように追加で記述する。
class F { public: static bool f( int a ) { return ( 0 == a ); } };
利用側では、こんな感じ。
A3<int, F> a3; bool b0 = a3.f( 0 ); // true bool b1 = a3.f( 1 ); // false
最適化で、g( int a )はインライン展開されて消えるので、実質f(T a)からF::f(int a)を呼び出ししていることになる。
class Fは、スタティックな関数だけで構成されるので、コンストラクター等の記述は不要だ。普通のC++コンパイラはコンパイルでそれらを生成しないだろう。
記述も最低必要な部分だけで済むので、メンテも簡単だ。
注意点として、上記のオブジェクトFはヘッダーで記述するか、利用するコードファイル内に配置しないと、オブジェクトの生成場所のコンパイルでエラーが発生する。
自分は少しだけ変形して使っている。
template <typename T> class F { public: static bool f( T a ) { return true; } }; template <typename T, class C> class A3 { private: bool g( T a ) { return C::f( a ); } public: A3() {} A3( const A3& lhs ) = default; A3( A3&& rhs ) = default; A3& operator=( const A3& lhs ) = default; A3& operator=( A3&& rhs ) = default; ~A3() = default; bool f( T a ) { return g(a); } }; template <typename T> using B3 = A3<T, F<T>>;
オブジェクトをintにするときには、以下のように追加で記述する。
template<> class F<int> { public: static bool f( int a ) { return ( 0 == a ); } };
利用側では、こんな感じ。
B3<int> b3; bool b0 = b3.f( 0 ); // true bool b1 = b3.f( 1 ); // false
利用時に特化関数オブジェクトを宣言に含めるか含めないかの違い程度だが、利用側のコードとしてはオブジェクトの型だけに気を配れば良いのがメリットだ。