Skip to content

类型与非类型模板

类型模板

在C++11中,我们可以使用using关键字来定义模板别名,这是一种比typedef更简洁和清晰的方式。模板别名可以让我们给一个复杂的模板类型起一个新的名字,从而提高代码的可读性和复用性。本文将介绍使用using来定义模板别名的语法和用法,并举一些实际的例子。

使用using来定义模板别名的语法如下:

cpp
template <typename T1, typename T2, ...>
using alias_name = some_template<T1, T2, ...>;

其中,T1, T2, ...是模板参数,alias_name是模板别名,some_template是一个已有的模板类型。例如,我们可以给std::vector<std::pair<int, std::string>>这样一个复杂的类型起一个简单的别名:

cpp
template <typename T>
using vec_pair = std::vector<std::pair<int, T>>;

这样,我们就可以用vec_pair<std::string>来代替std::vector<std::pair<int, std::string>>,从而简化代码的编写和阅读。注意,vec_pair是一个模板别名,它本身不是一个类型,而是一个类型模板。我们需要给它提供一个具体的类型参数,才能得到一个具体的类型。

使用using来定义模板别名有以下几个优点:

  • 使用using可以避免一些typedef不能做到的事情,比如重定义一个函数指针或者一个模板。例如:

    cpp
    // 使用typedef重定义一个函数指针
    typedef void (*func_t) (int, int); // OK
    
    // 使用using重定义一个函数指针
    using func_t = void (*) (int, int); // OK
    
    // 使用typedef重定义一个模板
    typedef std::map<std::string, int> map_int_t; // OK
    typedef std::map<std::string, T> map_t<T>; // Error
    
    // 使用using重定义一个模板
    using map_int_t = std::map<std::string, int>; // OK
    using map_t<T> = std::map<std::string, T>; // OK
  • 。而typedef则需要把新的名字放在中间,旧的类型放在两边。例如:
    cpp
    // 使用typedef重定义一个数组类型
    typedef int arr_t[10]; // OK
    
    // 使用using重定义一个数组类型
    using arr_t = int[10]; // OK
  • 使用using可以和其他C++11的特性结合使用,比如decltype和auto等。例如:

    cpp
    // 使用decltype和auto推断类型
    auto x = 10;
    decltype(x) y = 20;
    
    // 使用using给推断出的类型起别名
    using xy_t = decltype(x); // OK
    
    // 使用typedef给推断出的类型起别名
    typedef decltype(x) xy_t; // Error

总之,使用using来定义模板别名是一种方便和优雅的方法,它可以让我们更容易地处理复杂的模板类型,并提高代码的可读性和复用性。在C++11中,建议使用using来替代typedef,除非有特殊的原因。

非类型模板参数

template <typename/class T>中的typename/class表示T代表一个类型,是一个类型参数。在模板参数列表里面还可以定义非类型模板参数,非类型参数代表的是一个值。

既然非类型参数代表的是一个值,我们当然不能使用typename/class来修饰这个值。我们当然要用之前学过的基础数据类型来修饰它,比如s是一个int类型的值,我们可以这样写template <int s>

当模板被实例化时,这种非类型模板参数的值或者是用户提供的或者是编译器自动推断的,都有可能,要具体情况具体分析。但是,这些值都必须是常量表达式,因为实例化这些模板是编译时进行实例化的。

下面来看一个简单的使用非类型模板参数的实例:

cpp
#include <iostream>

template <int a, int b>
int funcadd(){
  int tmp = a + b;
  return tmp;
}

int main(int argc, char *argv[]){
  int ret = funcadd<12, 13>();
  std::cout << ret << std::endl;
    
    /**
     * 下面来写一个错误的方式
     *   这种方式来使用非类型模板参数并不能正确的编译程序,因
     *   为a是一个变量,它的值在程序编译时不能够确定;而实例化函数
     *   模板是在编译时干的事。
     */
    //int a = 12;
    //ret = funcadd<a, 13>();

  return 0;
}

程序输出:

shell
25

通过上面的实例可以看到,非类型模板参数在进行使用的时间并不是通过()来传递参数,而是在<>内进行参数的传递了;这种方式称为。但是并不是所有的非类型模板参数都需要显式的制定模板参数,有些时间编译器是可以自动推断出来的,只有在编译器推断不出来或者我们想要手动传递的情况下才会使用显式指定模板参数的方式。

理解了上面简单的非类型模板参数,下面我们再来看另外一个实例:

cpp
#include <iostream>

template <typename T, int a, int b>
int funcadd(T c){
  T tmp = c +  a + b;
  return tmp;
}

int main(int argc, char *argv[]){
  double ret = funcadd<double, 10, 12>(13);
  std::cout << ret << std::endl;

  return 0;
}

程序输出:

shell
35

这个实例中同时使用了类型模板参数和非类型模板参数,同时显式的指定了传入参数的类型并且在()中传递了相应类型的函数参数。接下来我们看一个没有显式指定传入参数的实例:

cpp
#include <iostream>
#include <cstring>

template <unsigned T1, unsigned T2>
int funccmp(const char(&p1)[T1], const char(&p2)[T2]){
  return strcmp(p1, p2);
}

int main(int argc, char *argv[]){
  int ret1 = funccmp("test1", "test1");
  int ret2 = funccmp("test1", "test2");
  int ret3 = funccmp("test1", "test");
  std::cout << ret1 << std::endl;
  std::cout << ret2 << std::endl;
  std::cout << ret3 << std::endl;

  return 0;
}

程序输出:

shell
0
-1
1

通过这个实例我们可以看出来,我们并没有指定传入的字符的个数,但是编译器在编译的时间自动为我们推断出了每次传入参数字符的个数然后取代T1T2

。编译器生成代码的时间需要能够找到函数的函数体,所以模板定义通常都会放在.h文件中。