概念
1.概述
模版元编程(template metaprogram)是C++中最复杂也是威力最强大的编程范式,它是一种可以创建和操纵程序的程序。模版元编程完全不同于普通的运行期程序,它很独特,因为模版元程序的执行完全是在编译期,并且模版元程序操纵的数据不能是运行时变量,只能是编译期常量,不可修改,另外它用到的语法元素也是相当有限,不能使用运行期的一些语法,比如if-else,for等语句都不能用。因此,模版元编程需要很多技巧,常常需要类型重定义、枚举常量、继承、模板偏特化等方法来配合,因此编写模版元编程比较复杂也比较困难。
现在C++11新增了一些模版元相关的特性,不仅可以让我们编写模版元程序变得更容易,还进一步增强了泛型编程的能力,比如type_traits让我们不必再重复发明轮子了,给我们提供了大量便利的元函数,还提供了可变模板参数和tuple,让模版元编程“如虎添翼”。本文将向读者展示C++11中模版元编程常用的技巧和具体应用。
2.模版元基本概念
模版元程序由元数据和元函数组成,元数据就是元编程可以操作的数据,即C++编译器在编译期可以操作的数据。元数据不是运行期变量,只能是编译期常量,不能修改,常见的元数据有enum枚举常量、静态常量、基本类型和自定义类型等。
元函数是模板元编程中用于操作处理元数据的“构件”,可以在编译期被“调用”,因为它的功能和形式和运行时的函数类似,而被称为元函数,它是元编程中最重要的构件。元函数实际上表现为C++的一个类、模板类或模板函数,它的通常形式如下:
template<int N, int M>
struct meta_func
{
static const int value = N+M;
}
调用元函数获取value值:cout«meta_func<1, 2>::value«endl;
meta_func的执行过程是在编译期完成的,实际执行程序时,是没有计算动作而是直接使用编译期的计算结果的。元函数只处理元数据,元数据是编译期常量和类型,所以下面的代码是编译不过的:
int i = 1, j = 2;
meta_func<i, j>::value; //错误,元函数无法处理运行时普通数据
模板元编程产生的源程序是在编译期执行的程序,因此它首先要遵循C++和模板的语法,但是它操作的对象不是运行时普通的变量,因此不能使用运行时的C++关键字(如if、else、for),可用的语法元素相当有限,最常用的是:
- enum、static const,用来定义编译期的整数常量;
- typedef/using,用于定义元数据;
- T、Args…,声明元数据类型;
- template,主要用于定义元函数;
- “::",域运算符,用于解析类型作用域获取计算结果(元数据)。
如果模板元编程中需要if-else、for等逻辑时该怎么办呢?
模板元中的if-else可以通过type_traits来实现,它不仅仅可以在编译期做判断,还可以做计算、查询、转换和选择。
模板元中的for等逻辑可以通过递归、重载、和模板特化(偏特化)等方法实现。
下面来看看C++11提供的模版元基础库type_traits。
3.type_traits
type_traits是C++11提供的模板元基础库,通过type_traits可以实现在编译期计算、查询、判断、转换和选择,提供了模板元编程需要的一些常用元函数。下面来看看一些基本的type_traits的基本用法。
最简单的一个type_traits是定义编译期常量的元函数integral_constant,它的定义如下:
template< class T, T v >
struct integral_constant;
借助这个简单的trait,我们可以很方便地定义编译期常量,比如定义一个值为1的int常量可以这样定义:
using one_type = std::integral_constant<int, 1>;
或者
template<class T>
struct one_type : std::integral_constant<int, 1>{};
获取常量则通过one_type::value来获取,这种定义编译期常量的方式相比C++98/03要简单,在C++98/03中定义编译期常量一般是这样定义的:
template<class T>
struct one_type
{
enum{value = 1};
};
template<class T>
struct one_type
{
static const int value = 1;
};
可以看到,通过C++11的type_traits提供的一个简单的integral_constant就可以很方便的定义编译期常量,而无需再去通过定义enum和static const变量方式去定义编译期常量了,这也为定义编译期常量提供了另外一种方法。C++11的type_traits已经提供了编译期的true和false,是通过integral_constant来定义的:
typedef integral_constant<bool, true> true_type;
typedef integral_constant<bool, false> false_type;
除了这些基本的元函数之外,type_traits还提供了丰富的元函数,比如用于编译期判断的元函数:
#include <iostream>
#include <type_traits>
int main() {
std::cout << "int: " << std::is_const<int>::value << std::endl;
std::cout << "const int: " << std::is_const<const int>::value << std::endl;
//判断类型是否相同
std::cout<< std::is_same<int, int>::value<<"\n";// true
std::cout<< std::is_same<int, unsignedint>::value<<"\n";// false
//添加、移除const
cout << std::is_same<const int, add_const<int>::type>::value << endl;
cout << std::is_same<int, remove_const<const int>::type>::value << endl;
//添加引用
cout << std::is_same<int&, add_lvalue_reference<int>::type>::value << endl;
cout << std::is_same<int&&, add_rvalue_reference<int>::type>::value << endl;
//取公共类型
typedef std::common_type<unsigned char, short, int>::type NumericType;
cout << std::is_same<int, NumericType>::value << endl;
return 0;
}
type_traits还提供了编译期选择traits:std::conditional,它在编译期根据一个判断式选择两个类型中的一个,和条件表达式的语义类似,类似于一个三元表达式。它的原型是:
template< bool B, class T, class F >
struct conditional;
用法比较简单:
#include <iostream>
#include <type_traits>
int main()
{
typedef std::conditional<true,int,float>::type A; // int
typedef std::conditional<false,int,float>::type B; // float
typedef std::conditional<(sizeof(long long) >sizeof(long double)),
long long, long double>::type max_size_t;
cout<<typeid(max_size_t).name()<<endl; //long double
}
另外一个常用的type_traits是std::decay(朽化),它对于普通类型来说std::decay(朽化)是移除引用和cv符,大大简化了我们的书写。除了普通类型之外,std::decay还可以用于数组和函数,具体的转换规则是这样的:
先移除T类型的引用,得到类型U,U定义为remove_reference::type。
- 如果is_array::value为 true,修改类型type为remove_extent::type *。
- 否则,如果is_function::value为 true,修改类型type将为add_pointer::type。
- 否则,修改类型type为 remove_cv::type。
std::decay的基本用法:
typedef std::decay<int>::type A; // int
typedef std::decay<int&>::type B; // int
typedef std::decay<int&&>::type C; // int
typedef std::decay<constint&>::type D; // int
typedef std::decay<int[2]>::type E; // int*
typedef std::decay<int(int)>::type F; // int(*)(int)
std::decay除了移除普通类型的cv符的作用之外,还可以将函数类型转换为函数指针类型,从而将函数指针变量保存起来,以便在后面延迟执行,比如下面的例子。
引子
交换迭代器1.0
template <class iter1,class iter2>
void my_swap (iter1 i1,iter2 i2)
{
iter1::value_type t = *i1;
*i1 = *i2;
*i2 = t;
}
要是参数类型不是迭代器,而是int*?
交换迭代器1.1
使用萃取器 iterator_traits
template <class iter1,class iter2>
void my_swap (iter1 i1,iter2 i2)
{
typename iterator_traits<iter1>::value_type t = *i1;
*i1 = *i2;
*i2 = t;
}
std::enable_if
C++11中引入了std::enable_if函数,函数原型如下:
template< bool B, class T = void >
struct enable_if;
可能的函数实现:
template<bool B, class T = void>
struct enable_if {};
template<class T>
struct enable_if<true, T> { typedef T type; };
//由上可知,只有当第一个模板参数为true时,enable_if会包含一个type=T的公有成员,否则没有该公有成员。
使用场景
1.类型偏特化
在使用模板编程时,经常会用到根据模板参数的某些特性进行不同类型的选择,或者在编译时校验模板参数的某些特性。
在使用模板编程时,可以利用std::enable_if的特性根据模板参数的不同特性进行不同的类型选择。
如下所示,我们可以实现一个检测变量是否为智能指针的实现:
#include <iostream>
#include <type_traits>
#include <memory>
template <typename T>
struct is_smart_pointer_helper : public std::false_type {};
template <typename T>
struct is_smart_pointer_helper<std::shared_ptr<T> > : public std::true_type {};
template <typename T>
struct is_smart_pointer_helper<std::unique_ptr<T> > : public std::true_type {};
template <typename T>
struct is_smart_pointer_helper<std::weak_ptr<T> > : public std::true_type {};
template <typename T>
struct is_smart_pointer : public is_smart_pointer_helper<typename std::remove_cv<T>::type> {};
template <typename T>
typename std::enable_if<is_smart_pointer<T>::value,void>::type check_smart_pointer(const T& t)
{
std::cout << "is smart pointer" << std::endl;
}
template <typename T>
typename std::enable_if<!is_smart_pointer<T>::value,void>::type check_smart_pointer(const T& t)
{
std::cout << "not smart pointer" << std::endl;
}
int main()
{
int* p(new int(2));
std::shared_ptr<int> pp(new int(2));
std::unique_ptr<int> upp(new int(4));
check_smart_pointer(p);// not smart pointer
check_smart_pointer(pp);// is smart pointer
check_smart_pointer(upp);// is smart pointer
return 0;
}
2.校验函数模板参数类型
有时定义的模板函数,只希望特定的类型可以调用,参考 cppreference 官网示例,很好的说明了如何限制只有整型可以调用的函数定义:
template <typename T>
typename std::enable_if<std::is_integral<T>::value, bool>::type
is_odd(T t) {
return bool(t%2);
}
template <typename T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
bool is_even(T t) {
return !is_odd(t);
}
一个通过返回值,一个通过默认模板参数,都可以实现校验模板参数是整型的功能。
3.控制函数返回类型(?)
对于模板函数,有时希望根据不同的模板参数返回不同类型的值,进而给函数模板也赋予类型模板特化的性质。典型的例子可以参看 tuple
的获取第 k 个元素的 get
函数:
template <std::size_t k, class T, class... Ts>
typename std::enable_if<k==0, typename element_type_holder<0, T, Ts...>::type&>::type
get(tuple<T, Ts...> &t) {
return t.tail;
}
template <std::size_t k, class T, class... Ts>
typename std::enable_if<k!=0, typename element_type_holder<k, T, Ts...>::type&>::type
get(tuple<T, Ts...> &t) {
tuple<Ts...> &base = t;
return get<k-1>(base);
}
不定参数
递归
template <typename T>
void trigger_func(const T &t) //结束递归函数
{
m_vector.Insert<T>(t);
}
template <typename T, typename... Args>
void trigger_func(const T &t, const Args &... rest)
{
m_vector.Insert<T>(t);
trigger_func(rest...); // 递归调用
}
得到不定参的长度
template <typename... Args>
void fun ()
{
cout << sizeof...(Args) <<endl;
}
判断是不是容器
1.偏特化
template<typename T>
void my_fun(T &obj)
{
perform1();
}
template<typename T>
void my_fun(std::vector<T> &obj)
{
perform2();
}
int main(void)
{
int a;
std::vector<int> b;
my_fun(a);
my_fun(b);
}
心得:
- 可以用is_same 判断类型后返回 类型名,用
- remove_reference
- remove_pointer
- remove_volatile
- remove_const
- 得到干净的类型,然后实现偏特化
- 结合enable_if判断某个类型具有某些特性
- 不定参用递归,可以遍历