Rokiのチラ裏

学生による学習のログ

Type Erasureにおける質問

とても有名な、Type Erasureによるダックタイピング。

#include<memory>
struct X{
    template<class _Tp>
    X(_Tp&& x):ptr(new Erasure<_Tp>(std::forward<_Tp>(x))){}
    void quack()const{ptr->quack();}
private:
    struct ErasureBase{
        virtual ~ErasureBase()=default;
        virtual void quack()const=0;
    };
    template<class _Tp>
    struct Erasure:ErasureBase{
        Erasure(const _Tp& x):ducky(x){}
        void quack()const override{ducky.quack();}
    private:
        _Tp ducky;
    };
    std::unique_ptr<ErasureBase> ptr=nullptr;
};

#include<iostream>
typedef struct{void quack()const{std::cout<<"gaaa"<<std::endl;}}Duck;
typedef struct{void quack()const{std::cout<<"...."<<std::endl;}}Foo;
inline void invoke(const X& ducky){ducky.quack();}

int main()
{
    invoke(Duck());
    invoke(Foo());  
}

このコードを見せると、以下のような質問を受けた。

  • 何故継承しなければならないのか。
  • 何故ポインタでなければならないのか。

この質問は、継承から成るポリモーフィズムなどその辺りを学ばなかったのか、それともそもそもTypeErasureという作戦の意図が理解できていないかのどちらかだと思うのだが...取り敢えず分かりやすいよう、上記の内容を質問に沿って単純化する。

#include<memory>

struct X{
    template<class _Tp>
    X(_Tp&& x):ptr(new Erasure<_Tp>(std::forward<_Tp>(x))){}
private:
    struct ErasureBase{
        virtual ~ErasureBase()=default;
    };
    template<class _Tp>
    struct Erasure:ErasureBase{
        _Tp data;
        Erasure(const _Tp& x):data(x){}
    };
    std::unique_ptr<ErasureBase> ptr=nullptr;
};

#include<string>
int main()
{
    using namespace std::string_literals;
    X a=42;
    X b="hoge"s;
}

この時、何故ptrが基底クラスであるのか...というように質問を解釈したのだが、寧ろどのようにしてptrを自身の型のポインタとして宣言するのか、その方法を考えて欲しい。

struct X{
// ... 略
    template<class _Tp>
    struct Erasure:ErasureBase{
        _Tp data;
        Erasure(const _Tp& x):data(x){}
    };
    std::unique_ptr<Erasure< ?? >> ptr=nullptr;
};

元々、メンバ変数をテンプレート型からインスタンス化する場合、必ず必要となるのはテンプレート引数に対する明示的な型指定である*1。それをargument deductionのみで解決させなければならない。それには、関数に渡ってきた値の型が何であれ格納できる型を、事前に宣言しなければならない。その条件に見合うものとして、テンプレートでない基底クラス型のポインタが当てはまる。

struct X{
    template<class _Tp>
    X(_Tp&& x):ptr(new Erasure<_Tp>(std::forward<_Tp>(x))){}
// ... 略
template<class _Tp>
    struct Erasure:ErasureBase{
        _Tp data;
        Erasure(const _Tp& x):data(x){}
    };
    std::unique_ptr<ErasureBase> ptr=nullptr;
// ... 略

これで見事、argument deductionのみでインスタンスを格納する事ができた。というお話。
後者の疑問もこれが理解できれば元々疑問にも思わない内容だと気づくはずだ。。。
個人的にはC++erは皆std::anyやらstd::function、スマポのカスタムデリータ等々を実用範囲外だとしてもフルスクラッチしてみたりするものだと思っていたので、この質問には少し驚いた。

*1:Default arguments for template parametersを除く