C++ 中的 SFINAE 技巧

                     

贡献者: 待更新

  • 本文缺少预备知识,初学者可能会遇到困难。

1. 限制模板范围

   例如我们有一个函数模板将两个对象 v1v2 相加得到 v

template <class T, class T1, class T2>
void Plus(T &v, const T1 &v1, const T2 &v2)
{...}
如果我们想要对不同类型的 T,T1,T2 执行不同的代码,我强烈推荐下面要介绍的 SFINAE 技巧来实现。

   我们先来定义一个宏函数 MY_IF()(至于为什么要这么定义先不介绍),其中使用了 type_traits 头文件中的 enable_if。这个宏用于输入一个 constexprbool 表达式,由于表达式中有可能出现一个或多个逗号,所以形式上我们用了任意变量的宏函数。

#define MY_IF0(...) typename std::enable_if<(bool)(__VA_ARGS__), Int>::type
#define MY_IF(...) MY_IF0(__VA_ARGS__) = 0
这里先定义了 MY_IF0(),看起来多此一举,但实际上在一些情况下我们也需要单独使用 MY_IF0()

   另外,我们假设存在一些函数模板用于判断 T, T1, T2 的类型(具体怎么定义先不介绍),例如如果 T 是一个矩阵,is_matrix<T>() 就返回 true,而 is_scalar<T>()is_vector<T>() 都返回 false

template <class T> bool is_scalar();
template <class T> bool is_vector();
template <class T> bool is_matrix();

   现在我们用 MY_IF 来区分不同版本的 Plus 函数。标量相加的函数如下

template <class T, class T1, class T2,
MY_IF(is_scalar<T>() && is_scalar<T1>() && is_scalar<T2>())>
void Plus(T &v, const T1 &v1, const T2 &v2)
{ v = v1 + v2; }

   注意在我们通过 MY_IF 声明了这个模板什么时候有定义(只有 T, T1, T2 为标量时有定义,例如 int, double, complex 等)。

   同样,我们可以再写一个版本的 Plus 定义矢量相加

template <class T, class T1, class T2,
MY_IF(is_vector<T>() && is_vector<T1>() && is_vector<T2>())>
void Plus(T &v, const T1 &v1, const T2 &v2)
{
    for (int i = 0; i < v.size(); ++i) {
        v[i] = v1[i] + v2[i];
    }
}

   我们还可以再定义矩阵相加,矩阵与标量相加,矢量与标量相加等等。

   如果我们的函数需要先 declare 再 define(例如 class 的成员函数在 class 定义中 declare,然后在别的地方 define),那就需要在 declaration 中使用 MY_IF(),而在 definition 中使用 MY_IF0(),其他内容都一样。

   使用 SFINAE 的好处是,我们可以限制函数模板 instantiate 的条件,使得一些不合法的使用变得没有定义(比如说我想要用 Plus 把矩阵和矢量相加,又比如复数矩阵相加得到实数矩阵)。另一个好处是无论我们定义多少个版本的 Plus,只要 MY_IF 中的条件总在唯一一个版本中为 true,编译器就不会抱怨无法判断使用哪个版本的 Plus 函数。

2. 类型判断

   我们下面来介绍这些函数如何实现。

   首先,它们必须是 constexpr 函数,也就是说它们必须要能在编译阶段(而不是运行阶段)被调用并返回结果。其次,它们是模板函数,因为类型 T 不可能作为函数参数,而只能作为模板参数。

   我们首先定义 is_same<T1, T2>() 函数模板来判断 T1, T2 两个类型是否相同(这里用到了 标准库的 type_traits 头文件)

template <class T1, class T2>
constexpr bool is_same()
{ return std::is_same<T1, T2>::value; }

   注意在 type_traits 中,is_ 开头的函数都是类模板而不是函数模板,理论上我们可以直接拿 std::is_same 来用,但为了概念和使用上更简单,我们重新定义了同名的函数模板。

   有了 is_same,我们就可以很容易地实现 is_int<T>(), is_double<T>(), 等。

template <class T>
constexpr bool is_int()
{ return is_same<T, int>(); }

template <class T>
constexpr bool is_double()
{ return is_same<T, double>(); }

   is_complex<T>() 有所不同,因为 std::complex 本身也是一个类模板,可以有不同的类。这里为了简单,姑且就假设我们只使用 std::complex<double>。如果需要支持任意类型的 std::complex<>,需要使用下文中定义 is_vector 的方法。

template <class T>
constexpr bool is_complex()
{ return is_same<T, std::complex<double>>(); }

   为了简单起见,我们假设 “标量” 只包括 int, double, std::complex<double> 三种,于是可以定义用于判断标量的函数

template <class T>
constexpr bool is_scalar()
{ return is_int<T>() || is_double<T>() || is_complex<T>(); }

   现在我们再来看如何定义 is_vector<T>()。对于矢量,我们既可以使用 std::vector<>,也可以自己定义一个矢量类型。我们通过 template specialization 来实现 is_vector<>

template <class T> struct is_vector_imp : std::false_type {};
template <class T> struct is_vector_imp<vector<T>> : std::true_type {};
template<class T>
constexpr bool is_vector()
{
    return is_vector_imp<T>();
}

   首先我们对一般的类型 T 定义的 is_vector_imp(imp 这里表示 implementation),继承 std::false_type(你只需要知道 falst_type 是一个类,它的对象可以自动转换为 false,true_type 类的对象可以自动转换为 true)。然后对凡是符合 is_vector_imp<vector<T>> 格式的类进行不同的定义:即继承 true_type。最后,我们再把 is_vector_imp 已经实现的功能封装成函数模板 is_vector<T>(),就大功告成了。

   用于判断矩阵类型的 is_matrix<T>() 也可以如法炮制

template <class T> struct is_matrix_imp : std::false_type {};
template <class T> struct is_matrix_imp<Matrix<T>> : std::true_type {};
template<class T>
constexpr bool is_matrix()
{
    return is_vector_imp<T>();
}
然而注意标准库中没有矩阵类型(据说其实有一个,后来烂尾了,几乎没人用),所以我们一般自己定义矩阵类型 Matrix<T>

                     

© 小时科技 保留一切权利