Some Notes of TMP

看了些有关模板元编程的东西,写点笔记

一个视频

定义

元编程指通过编写或操纵其它程序或自身作为数据, 或者在运行时完成部分本该在编译时完成的工作的编程方式. (来自 Wikipedia)

C++ 中的模板元编程 (Template MetaProgramming, TMP) 使用模板的实例化来进行编译期的求值.

模板元编程通常用来提高代码的灵活性和提升运行时速度 (因为一部分计算放到了编译期中).

模板元编程是图灵完备的,通过模板特化实现分支, 通过递归实现循环,同时由于是编译期的求值,其中的参数, 中间量和返回值均是不可变的. 从这些角度来说模板元编程有点类似于函数式编程.

一些例子

求两个数的最大公因数 gcd<m, n>

1
2
3
4
5
6
7
8
9
template<unsigned m, unsigned n>
struct gcd {
static unsigned constexpr value = gcd<n, m%n>::value;
};
template<unsigned m>
static gcd<m, 0> {
static_assert(m != 0);
static unsigned constexpr value = m;
}

返回值为值的元函数也可以使用 constexpr 函数代替:

1
2
3
constexpr unsigned gcd(unsigned a, unsigned b) {
return b == 0 ? a : gcd(b, a%b);
}

获得去除 const 的类型 remove_const<T>

1
2
3
4
template<class T>
struct remove_const { using type = T; }
template<class T>
struct remove_const<T const> { using type = T; }

获得类型本身 identity<T>

1
2
template<class T>
struct identity<T> { using type = T; }

获得某类型的一个值 integral_constant<T, v>

1
2
3
4
5
6
7
template<class T, T v>
struct integral_constant {
static constexpr T value = v;
constexpr operator T () const noexcept { return value; }
constexpr T operator ()() const noexcept { return value; }
...;
};

bool_constant<b>

1
2
template<bool b>
using bool_constant = integral_constan<bool, b>;

true_typefalse_type

1
2
using true_type = bool_constant<true>
using false_type = bool_constant<false>

这两个类型可以简化元函数的编写,例如:

判断两个类型是否一致 is_same<T, U>

1
2
3
4
template<class T, class U>
struct is_same : false_type {};
template<class T>
struct is_same<T, T> : true_type {};

判断某类型是否在类型列表中 is_one_of<T, Ts...>

1
2
3
4
5
6
7
template<class T, class... Ps> struct is_one_of;
template<class T>
struct is_one_of<T> : false_type {};
template<class T, class... Ps>
struct is_one_of<T, T, Ps...> : true_type {};
template<class T, class P0, class... Ps>
struct is_one_of<T, P0, Ps...> : is_one_of<T, Ps...> {};

判断类型是否为 void 类型 is_void<T>(type trait)

1
2
3
4
5
template<class T> struct is_void : false_type {};
template<> struct is_void<void> : true_type {};
template<> struct is_void<void const> : true_type {};
template<> struct is_void<void volatile> : true_type {};
template<> struct is_void<void const volatile> : true_type {};

或者:

1
2
3
4
5
6
template<class T> struct is_void :
is_one_of<T,
void,
void const,
void volatile,
void const volatile> {};

若定义了 remove_cv<T>(去除 constvolatile) 也有:

1
2
3
template<class T> struct is_void :
is_same<remove_cv_t<T>, void>
{};

头文件 <type_traits> 中定义了许多类似的 type trait, 由于性能原因绝大多数 type trait 的实现都是基于编译器的内建方法.

元函数的调用

通过获取结构体中的公共成员以” 调用” 元函数.

1
2
gcd<8, 12>::value;
typename remove_const<int const>::type xxx;

由于模板可被重载, 需要使用 typename 关键字指定::type 为类型.

一些约定

返回值为值的模板函数, 返回值放在公开的静态成员常量 value 中, 或者直接继承自 std::integral_constant.

返回值为类型的模板函数, 返回值放在公开的成员类型 type 中.

一般需要一个以_v(值) 或_t(类型) 结尾的的模板常量以方便调用:

1
2
3
4
5
template<unsigned m, unsigned n>
constexpr unsigned gcd_v = gcd<m, n>::value;

template<class T>
using remove_const_t = typename remove_const<T>::type;

不要对标准库中的 type trait 特化,这样可能导致未定义行为.

SFINAE

SFINAE means Substitution Failure Is Not An Error.

当模板实例化时,编译器首先会寻找模板类型参数,有如下三种情形:

  • 使用显式的模板类型参数;
  • 从函数的实参中推断类型;
  • 使用默认的模板类型参数.

找到模板参数后则会依次查找模板的定义, 使用找到的模板参数替换定义中的模板参数. 若替换过程中产生的代码有意义则实例化成功, 否则称发生了替换失败 (Substitution Failure), 编译器不会抛出错误 , 将继续寻找其它定义.

假设定义了元函数 enable_if<b, T>

1
2
3
4
template<bool, class T = void>
struct enable_if : identity<T> {};
template<class T>
struct enable_if<false, T> {};

注意 false 时结构体无 type 成员.

假设某算法 f 需要对整型与浮点型数据进行重载,则有:

1
2
3
4
template<class T>
enable_if_t<is_integral_v<T>, int> f (T val) {...;}
template<class T>
enable_if_t<is_floating_point_v<T>, double> f (T val) {...;}

若传入的参数为整型则第二条中的 enable_if_t 将生成错误的代码 (对无 type 成员的结构体取 type), 发生 SFINAE, 寻找其它定义,反之亦然. 但当传入的参数为其它的类型 (例如字符串), 则两个定义均不成立, 将发生编译错误.

C++20 以后可以使用 concept 和 requires 子句替代上述写法:

1
2
3
4
5
template<integral T>    // concept
int f (T val) {...;}
template<class T>
requires floating_point<T> // requires clause
double f (T val) {...;}

非求值运算

对于例如 sizof, decltype 之类的运算符, 其运算对象在编译期和运行时均不求值. 这意味着这类运算符只会检查运算对象的声明.

其中 decltype()std::declval<T>() 是一对十分有用的非求值运算. decltype() 将返回括号内表达式的返回值类型 , std::declval<T>() 将返回一个类型为 T 的右值引用 (std::declval<T&>() 可返回左值引用, 由于引用折叠).

用例: 判断某类型是否能够复制赋值 is_copy_assignable<T>

1
2
3
4
5
6
7
8
9
10
template<class T>
struct is_copy_assignable {
private:
template<class U,
class = decltype(declval<U&>() = declval<U const&>())>
static true_type try_assignment(U&&);
static false_type try_assignment(...);
public:
using type = decltype(try_assignment(declval<T>()));
};

在没有 decltype 时 (C++11 以前) 则需要使用 sizeof, 通过返回类型的大小来区分真假.

void_t

void_t 的定义如下:

1
template<class...> using void_t = void;

当模板参数均有意义时则返回 void 类型, 否则将引发 SFINAE.

用例:判断某类型是否有公开成员类型 type:

1
2
3
4
template<class T, class = void>
struct has_type : false_type {};
template<class T>
struct has_type<T, void_t<typename T::type>> : true_type {};

用例:重写 is_copy_assinable

1
2
3
4
5
6
7
8
9
template<class T>
using copy_assignment_t =
decltype(declval<T&>() = declval<T const &>())
template<class T, class = void>
struct is_copy_assignable : false_type {};
template<class T>
struct is_copy_assignable<T,
void_t<copy_assignment_t<T>>> :
is_same<copy_assignment_t<T>, T&> {};

注意模板默认参数必须与 void_t 的返回类型匹配 , 若不匹配模板特化将失效.

在 C++20 以后也可以使用 concept 与 requires 子句替代:

1
2
3
4
5
6
7
8
template<class T>
concept copy_assignable = requires(T& a, T const & b)
{ a = b; };
template<class T>
struct is_copy_assignable : false_type {};
template<class T>
requires copy_assignable<T>
struct is_copy_assignable<T> : true_type {};