图

C++ 中的 SFINAE 技巧

限制模板范围

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

1
2
3
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 表达式, 由于表达式中有可能出现一个或多个逗号,所以形式上我们用了任意变量的宏函数.

1
2
#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

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

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

1
2
3
4
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 定义矢量相加

1
2
3
4
5
6
7
8
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 函数.

类型判断

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

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

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

1
2
3
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>(), 等.

1
2
3
4
5
6
7
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 的方法.

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

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

1
2
3
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<>

1
2
3
4
5
6
7
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>() 也可以如法炮制

1
2
3
4
5
6
7
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>

致读者: 小时物理百科一直以来坚持所有内容免费且不做广告,这导致我们处于日渐严重的亏损状态。长此以往很可能会最终导致我们不得不选择商业化,例如大量广告,内容付费,会员制,甚至被收购。因此,我们鼓起勇气在此请求广大读者热心捐款,使网站得以健康发展。如果看到这条信息的每位读者能慷慨捐助 10 元,我们几天内就能脱离亏损状态,并保证网站能在接下来的一整年里向所有读者继续免费提供优质内容。感谢您的支持。
—— 小时(项目创始人)

编辑词条 返回目录 返回主页 捐助项目 © 小时物理百科 保留一切权利