Rokiのチラ裏

学生による学習のログ

const lvalue reference constructorとuniversal reference in constructorの共存における問題の解決法

帰ったらブログにでも書くよと言ってしまったので書く...
例題として、TypeErasureを利用した簡単なAnyを示す。こういったTypeErasureにはテンプレートコンストラクタを必要とするが該当部分でuniversal reference constructorを用いてPerfect Forwardingを実現している。

#include<utility>
#include<memory>

struct Any final{
    template<class _Tp>
    Any(_Tp&& x)
        :ptr(new Erasure<_Tp>(std::forward<_Tp>(x)))
    {}
    Any(const Any& any)
        :ptr(any.ptr.get()?any.ptr->clone():nullptr)
    {}
private:
    struct ErasureBase{
        virtual ~ErasureBase()=default;
        virtual ErasureBase* clone()=0;
    };
    template<class _Tp>
    struct Erasure:ErasureBase{
        _Tp data;
        explicit Erasure(const _Tp& x)
            :data(x)
        {}
        ErasureBase* clone()override{return new Erasure(data);}
    };
    std::shared_ptr<ErasureBase> ptr=nullptr;
    template<class _Tp> friend _Tp get(const Any&);
};

template<class _Tp>
_Tp get(const Any& any)noexcept{return std::dynamic_pointer_cast<Any::Erasure<_Tp>>(any.ptr)->data;}

#include<iostream>
int main()
{
    Any a=10;
    Any b=a;
    std::cout<<get<int>(a)<<std::endl<<get<int>(b)<<std::endl;
}

しかしこのコード、コンパイルは通るが実行するとsegmentation faultする。なぜか。
コピーコンストラクタをdeleteしてみよう。

Any(const Any& any)=delete;
a.cpp: In instantiation of ‘Any::Erasure<_Tp>::Erasure(const _Tp&) [with _Tp = Any]’:
a.cpp:7:8:   required from ‘Any::Any(_Tp&&) [with _Tp = Any]’
a.cpp:35:8:   required from here
a.cpp:21:11: エラー: use of deleted function ‘Any::Any(const Any&)’
    :data(x)
           ^
a.cpp:9:2: 備考: ここで宣言されています
  Any(const Any& any)=delete;
  ^~~

なんと、_TpがAny型に推論されている上、コピーコンストラクタが呼ばれずにuniversal reference in constructorが割り込んでいるではないか。問題を更に簡略化してみれば以下の通り。

#include<iostream>
template<class _Tp> void f(_Tp&&){std::cout<<"template :"<<__func__<<std::endl;}
void f(const int&){std::cout<<"non-template :"<<__func__<<std::endl;}

int main()
{
    f(42);
}

これはどちらが呼ばれるだろうか。

template :f

テンプレート版が呼ばれてしまった...というところまでが今回のお題。
これを回避する方法。

SFINAE発動

#include<iostream>
#include<utility>

template<class _Tp>
void f(_Tp&&,
        std::void_t<
        typename std::enable_if<!std::is_same<int,typename std::decay<_Tp>::type>::value>::type>* =nullptr)
{
    std::cout<<"template:"<<__func__<<std::endl;
}

void f(const int&){std::cout<<"non-template:"<<__func__<<std::endl;}

int main()
{
    f(10);
    f("hoge");
}
non-template:f
template:f

Anyにも同じく適用する。

struct Any{
    template<class _Tp>
    Any(_Tp&& x,
            std::void_t<
                typename std::enable_if<!std::is_same<Any,typename std::decay<_Tp>::type>::value>::type
                >* =nullptr)
        :ptr(new holder<_Tp>(std::forward<_Tp>(x)))
    {}
// ...

結論

Effective Modern C++を読むべき。