CppCon - Modern Template Metaprogramming 杂记

      2014年底才看到github和channel9上有CppCon2014的视频和资料,顿时激动不已。最近小生也一直在研习CppCon2014中令人兴奋的内容。这篇鄙文就是小生学习了《Modern Template Metaprogramming》之后,有对Unevaluated Operands更深刻的理解,有感而写。

      C++98标准中的Unevaluated Operands,只有sizeof操作符。C++11又引入了decltype,typeid和noexcept。Unevaluated Operands不会有求值的计算,即使是在编译期间。这意味着Unevaluated Operands中的表达式甚至不会生成具体的C++代码,操作符中的表达式仅需要声明而无需定义。在模板元编程中,我们有时候经常仅需要对catch a glimpse of一个表达式,而这些操作符都是很好的工具。

      下面我们以std::is_copy_assignable为例来看看Unevaluated Operands的威力。

namespace cpp11
{
    template <typename T>
    struct is_copy_assignable
    {
    private:
        template <typename U, typename = decltype(std::declval<T&>() = std::declval<T const&>())>
        static std::true_type try_assign(U&&);

        static std::false_type try_assign(...);

    public:
        using type = decltype(try_assign(std::declval<T>()));
    };
}

      我们以c++11的版本开始。这个std::is_copy_assignable的实现原理就是SFINAE。给定一个C++类型T,如果T存在一个有效的operator=,那么成员模板函数try_assign的第二个模板参数中decltype里面的表达式将是well formed,decltype将会推导出这个表达式的类型。而第二个非模板的普通成员函数try_assign的形参使用" ..." 是因为这种情况是最差匹配的情况,is_copy_assignable需要优先匹配成员模板函数try_assign。元函数返回type时,再次使用了decltype,这里是真正的点睛之笔。由于decltype无需求值,因此try_assign无需一个函数体。std::declval是C++11引入universal reference的一个附属品,它也是一个模板函数同样没有函数体,返回值的类型是一个T&&(universal reference),try_assign中使用std::declval可以让编译器知道我们传递给这个函数的参数类型,但没有真正的去构造一个对象,就好像是假设我传递一个实参给try_assign. 在Unevaluated Operands中的表达式如果是一个函数调用,那么std::declval是一个非常好的工具,姑且可以将它当做一个惯用法。那么如果这个T不存在一个有效的operator=,编译器将使用SFINAE选择普通的成员函数。最后元函数返回的type在T::operator= 存在的情况下是std::true_type,反之则是false_type.

      在c++98的年代,Unevaluated Operands只有sizeof,那么怎么实现呢。同使用下面这个例子或者类似的实现方法。

namespace cpp98
{
    namespace detail
    {
        template <typename T>
        T declval();
    }

    template <typename T>
    struct is_copy_assignable
    {
    private:

        typedef char One;
        typedef struct { char a[2]; } Two;

        template <int N = sizeof(detail::declval<T&>() = detail::declval<T const&>())>
        static One try_assign(int);

        static Two try_assign(...);

    public:

        typedef typename std::conditional<sizeof(try_assign(0)) == sizeof(One),
            std::true_type, std::false_type>::type type;
    };
}

      sizeof只能推导出类型的大小,因此try_assign不能再返回std::true_type和st::false_type,而是返回了两个大小不同的类型。实现原理依旧是SFINAE,而元函数返回的类型是根据try_assign返回类型的大小来决定导出std::true_type还是std::false_type.由于cpp98没有右值引用,所以这里自己实现了一个declval的替代版。在c++98的版本中,元函数的类型计算都是由sizeof来驱动的。c++11引入了更多的非求值操作符后,元编程确实方便了不少。

     在《Modern Template Metaprogramming》这篇演讲中还有一个非常有趣的模板工具,就是void_t.  它的实现可能如下代码所示

template <typename ... Args>
struct make_void 
{
    typedef void type;
};
    
template <typename ... Args>
using void_t = typename make_void<Args...>::type;

或者,

template <typename ... Args>
using void_t = void;

      让我们先来看看使用void_t来实现我们的is_copy_assignable. 

namespace cpp1y
{
    template <typename ... Args>
    struct make_void 
    {
        typedef void type;
    };
    
    template <typename ... Args>
    using void_t = typename make_void<Args...>::type;

    template <typename T, typename = void>
    struct is_copy_assignable : std::false_type
    {};

    template <typename T>
    struct is_copy_assignable<T, void_t<decltype(std::declval<T&>() = std::declval<T const&>())>> : std::true_type
    {
    };
}

       利用void_t实现的版本又要比使用SFINAE的版本实现简单许多。给定一个C++类型T,传递到元函数is_copy_assignable中,由于这个版本的元函数有两个参数,那么is_copy_assignable<T>被推导为is_copy_assignable<T,void>. 接下来,编译要检查是否有more specialized的特化版本。如果T有一个有效的operator=,那么is_copy_assignable的特化版本中的第二个参数中的decltype推导的表达式是well formed,那么decltype计算类型并与void_t共同推导出void. 由于特化版本是more specialized,因此编译器选择这个特化版本,并调用元函数std::true_type。反之,如果T没有一个有效的operator=,那么decltype推导的表达式无效,因此这种情况下的is_copy_assignable没有特化版本并调用元函数std::false_type。这里要注意的是,第二个默认的模板参数必须要以void为缺省的实参类型,因为void_t实际上就是void,才能让编译器找到特化的版本。换而言之,缺省的类型要与void_t一致,如果实现一个类似的int_t,那么这个缺省的类型应该是int. 

      小生在MSVC12实验的void_t版本一直有问题,还有更简单版本的void_t实现在MSVC和gcc的编译器上都不对,根据这篇演讲的说法是还需要一个提案,详见CWG 1558, treatment of unused arguments in an alias template specialization.  错误都一样,无论T是否有可用的operator=,这个元函数始终选择特化版本,即都是true_type,可能是因为void_t模板没有使用任何模板参数的原因吧,还需要进一步的研究。

      这是最后的实验代码和运行结果如下:

      http://ideone.com/ZUCvhY

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。