概念

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判断某个类型具有某些特性
  • 不定参用递归,可以遍历