Rokiのチラ裏

学生による学習のログ

クラスの内部に持つデータ型の互換性からクラス自体に互換性を持たせる場合の良き書き方

以下のような単純なクラスがある。

template<class _Tp>
struct X{
    X(const _Tp& x=0):value(x){}
    bool eq(const _Tp& x){return x==value;}
private:
    const _Tp value;
};

これを使う場合に、例えばintとlongを型として当てはめた場合、クラス自体の互換性を持つようにしたい。

X<int> a;
X<long> b=a; // 上の実装だとアウト

これを実現するには二つの方法がある。

template<class _Tp>
struct X{
    X(const _Tp& x=0):value(x){}
    bool eq(const _Tp& x)const{return x==value;}
    
    operator X<long>()const{return value;}
private:
    const _Tp value;
};

template<class _Tp>
struct Y{
    Y(const _Tp& x=0):value(x){}
    bool eq(const _Tp& x)const{return x==value;}

    Y(const Y<int>& x):value(x.value){}
private:
    const _Tp value;
};

operator Tを指定してメゾットとして含む方法と、コンストラクタの引数を指定してしまう方法である。どちらも同じように扱う事ができる。

#include<cassert>
int main()
{
    X<int> a(10);
    X<long> b=a;

    Y<int> c(20);
    Y<long> d=c;
    
    assert(a.eq(10) && b.eq(10));
    assert(c.eq(20) && d.eq(20));
}

この場合までであればYよりもXの方が多少コード量が少なくて済むぐらいで、動作自体はあまり変わらないが、例えば、インタフェースにvalueを持たせ、継承した場合は、どうだろうか。

template<class _Tp>
struct intfc{
    intfc(const _Tp& x):value(x){}
    virtual bool as(const _Tp&)const=0;
protected:
    const _Tp value;
};

template<class _Tp>
struct X final:intfc<_Tp>{
    using intfc<_Tp>::intfc;
    operator X<long>()const{return this->value;}
    
    bool as(const _Tp& x)const override{return x==this->value;}
};

Xはそのままで実装する事ができる。しかし、Yでは実装不可能である。

template<class _Tp>
struct Y final:intfc<_Tp>{
    Y(const _Tp& x):intfc<_Tp>::intfc(x){}
    Y(const Y<int>& x){this->value=x.value;} // 許されない
    
    bool as(const _Tp& x)const override{return x==this->value;}
};

two phase name lookup回避のため自身のクラスに所属しているか基底クラスに存在しているかを明記しなければならない。コンストラクタでの変数初期化時点ではthisや基底クラスを明示する事はできない上、const variableに対する代入は許されない。CRTPを使い基底クラスで派生クラスの互換性を持たせたいテンプレート型の引数を受け付けるようにすれば実現できるが、そもそも基底クラスの実装を派生クラスごとに変動させるのは論外である。

template<class _Tp,class Derived>
struct intfc{
    intfc(const _Tp& x):value(x){}
    intfc(const Derived& x):value(x.value){}
protected:
    const _Tp value;
};

template<class _Tp>
struct Y final:intfc<_Tp,Y<_Tp>>{
    Y(const _Tp& x):intfc<_Tp,Y<_Tp>>::intfc(x){}
    Y(const Y<int>& x):intfc<_Tp,Y<_Tp>>::intfc(x){}
};

まあ要するに、Xのような実装の方が、インタフェースに実データを持つクラスの、ある特定の互換性を作りたい場合において有効的な実装であるという事だ。

追記

本エントリの題材とは無関係になるが、CRTPで実装を強要する形を作って見た。

template<class _Tp,template<class> class Derived>
struct intfc{
    intfc(const _Tp& x=0):value(x){}
    
    bool eq(const _Tp& x)const{return static_cast<Derived<_Tp>*>(const_cast<intfc*>(this))->eq_impl(x);}
    
    template<class _U>
    bool eq(const Derived<_U>& x)const
    {
        return reinterpret_cast<Derived<_U>*>(const_cast<intfc*>(this))->eq_impl(x);
    }
protected:
    const _Tp value;
};

template<class _Tp>
struct X final:intfc<_Tp,X>{
    using intfc<_Tp,X>::intfc;
    operator X<long>()const{return this->value;}

    bool eq_impl(const _Tp& x)const{return this->value==x;}
    
    template<class _U>
    bool eq_impl(const _U& x)const{return this->value==x.value;}
};

#include<iostream>

int main()
{
    X<int> a=10;
    X<long> b=a;
    std::cout<<std::boolalpha<<a.eq(b)<<std::endl;
}

本質的な処理は単なる比較処理であるのでconst指定をして派生クラスへのキャストを行うためまずconst外しにconst_cast、最後は強引に派生クラスをポイントするためreinterpret_castを用いた。とてつもなく泥臭い。