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 {};