Keywords: CRTP SFINAE David Abrahams
以上关键词你如果都没听说过证明你还不够了解模板。SFINAE(Substitution Failure Is Not An Error)是boost模板库中常用的trick,记录如下:
假设我们想实现自己的泛型向量四则运算操作(举例而已,复杂向量/矩阵运算boost::ublas中已有实现),则可以首先定义如下:
// vector subtraction
template <typename T >
std::vector<T> operator-(const std::vector<T> &lhs, const std::vector<T> &rhs)
{
typedef typename std::vector<T>::size_type size_t;
size_t num_dims=lhs.size();
if ( num_dims != rhs.size() )
throw std::range_error("two dimensions did not match!");
size_t i;
std::vector<T> res(num_dims);
for (i=0;i<num_dims;i++)
{
res[i]=lhs[i]-rhs[i];
}
return res;
}
这样的定义仅仅够用,但还不够boost,因为仅能操作于std::vector上,实际上只要符合boost::range中Random Access Range Concept的绝大部分容器都应该可以应用于该操作上,于是将上述定义改写如下:
// vector subtraction
template <typename Container>
Container operator-(const Container &lhs, const Container &rhs)
{
typedef typename Container::size_type size_t;
size_t num_dims=lhs.size();
if ( num_dims != rhs.size() )
throw std::range_error("two dimensions did not match!");
size_t i;
Container res(num_dims);
for (i=0;i<num_dims;i++)
{
res[i]=lhs[i]-rhs[i];
}
return res;
}
这样定义看上去更泛型了,但是问题随之而来了,因为没有对模板参数进行类型检查,这个function signature实际上可以通过函数重载决议匹配任何不相关的类,
Class My_Data{};
My_Data md_1,md_2;
auto ret=md_1-md_2;// 错误,My_Data不是随机容器,而编译器的函数重载决议却匹配了上述定义的函数operator-
错误的原因是模板参数Container过于宽泛了,在函数重载决议的时候对几乎任何类型都是可见的(也就是在所属作用域的任何operator-候选匹配函数集里),为了根据Container的类型动态的调整自定义的operator-操作符重载时的可见域 ,boost::type traits和boost::enable_if就派上用场了.(模板参数类型检查应该使用boost::concept check,但是在这里如果使用容器类型不符合条件就无法通过编译,仍然无法满足我们的要求)
template <typename Container>
typename boost::lazy_enable_if<
boost::is_same<
typename std::iterator_traits<typename Container::iterator_type>::iterator_category,
typename std::random_access_iterator_tag
>,
Container>::type operator-(const Container &lhs, const Container &rhs)
…// 函数定义
这里利用了non-intrusive idiom-- SFINAE(Substitution Failure Is Not An Error)来动态调整function overload resolution的candidate set,当模板实参不为随机访问容器时函数的返回值将未定义但编译不会出错(SF Is Not An Error),根据标准该函数signatrue将不会加入重载函数集里面,由此解决了Container匹配任意类型的问题。
使用enable_if控制重载函数集有3种形式:
1. 返回值里面使用
2. 函数增加一个带默认值的参数用来匹配模板类型(不适用于operator overload)
3. 模板参数里面增加一个带默认值的模板参数。(对不支持C++ 0x模板参数默认值的老编译器不兼容)
这里使用了用得最多的第1种方法。这个实现不完美,没有像boost::concept check那样严格按random access iterator的定义来检查容器关联的iterator(如果想更加严谨应该自己定义一个concept check class,然后按照Concept定义逐条检查),但已经够用了。
SFINAE具体细节解释可以参见:
http://stackoverflow.com/questions/982808/c-sfinae-examples
由Sono Buoni提供的文档里面有具体的说明以及众多例子http://www.semantics.org/once_weakly/w02_SFINAE.pdf
SFINAE和sizeof操作符结合就可以实现对模板实参各种具体类型的检测,这个技巧首先是由Paul Mensonides在2002年3月发现的,早已经成为了type traits库的实现方法之一了(另一个是template partial specification, sizeof+SFINAE作为当编译器不支持partial specification时的workaround):
http://stackoverflow.com/questions/2127693/sfinae-sizeof-detect-if-expression-compiles
2.boost里面的Range Concept概念,对iterator按读写类型以及遍历特性两类特点进一步细分了容器和迭代器的概念,是对STL容器和迭代器概念的细化和refinement:
http://www.boost.org/doc/libs/1_48_0/libs/iterator/doc/new-iter-concepts.html
3. return type is also part of function signature when that function is a template function,这里面说得很清楚,包括对ISO 03涉及该点的具体条款的全部引用:
http://stackoverflow.com/questions/290038/is-the-return-type-part-of-the-function-signature
4. 今天测试代码的时候发现即使const T&(如果T不是move constructible的话)因为type safety的原因不能绑定到rvalue reference,具体原因可以看proposal #2831:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2831.html,作者为Douglas Gregor(gcc作者之一), David Abrahams(boost第1人,iso委员会成员)
总结:以上内容属于MPL库常识,掌握他们基本上不需要任何智力,只需要有足够的好奇心和英文文档阅读能力,花一定的时间进行学习。