Skip to content

类模板

本节将介绍类模板

初识类模板

类模板不是类,只有实例化类模板,编译器才能生成实际的类。

定义类模板

下面是一个类模板,它和普通类的区别只是多了一个 template<typename T>

cpp
template<typename T>
struct Test{};

和函数模板一样,其实类模板的语法也就是:

cpp
template< 形参列表 > 类声明

几乎所有我们前面讲的,函数模板中形参列表能写的东西,类模板都可以

同样的,我们的类模板一样可以用 class 引入类型形参名,一样不能用 struct

cpp
template<class T>
struct Test{};

使用类模板

下面展示了如何使用类模板 Test

cpp
template<typename T>
struct Test {};

int main(){
    Test<void> t;
    Test<int> t2;
    //Test t;       // Error!
}

我们必须显式的指明类模板的类型实参,并且没有办法推导,事实上这个空类在这里本身没什么意义。

或许我们可以这样:

cpp
template<typename T>
struct Test{
    T t;
};

这理所应当,类模板能使用类模板形参,声明自己的成员,那么如何使用呢?

cpp
// Test<void> t;  // Error!
Test<int> t2;     
// Test t3;       // Error!
Test t4{ 1 };     // C++17 OK!
  • Test<void> 我们稍微带入一下,模板的 TvoidT t 是?所以很合理
  • Test t4{ 1 }; C++17 增加了类模板实参推导,也就是说类模板也可以像函数模板一样被推导,而不需要显式的写明模板类型参数了,这里的 Test 被推导为 Test<int>

不单单是聚合体,当然,写构造函数也可以:

cpp
template<typename T>
struct Test{
    Test(T v) :t{ v } {}
private:
    T t;
};

类模板参数推导

这涉及到一些非常复杂的规则,不过我们不用在意。

对于简单的类模板,通常可以普通的类似函数模板一样的自动推导,比如前面提到的 Test 类型,又或者下面:

cpp
template<class T>
struct A{
    A(T, T);
};
auto y = new A{1, 2}; // 分配的类型是 A<int>

new 表达式中一样可以。

同样的可以像函数模板那样加上许多的修饰:

cpp
template<class T>
struct A {
    A(const T&, const T&);
};

多的就不用再提。

用户定义的推导指引

举个例子,我要让一个类模板,如果推导为 int,就让它实际成为 size_t:

cpp
template<typename T>
struct Test{
    Test(T v) :t{ v } {}
private:
    T t;
};

Test(int) -> Test<std::size_t>;

Test t(1);      // t 是 Test<size_t>

如果要类模板 Test 推导为指针类型,就变成数组呢?

cpp
template<typename T>
Test(T*) -> Test<T[]>;

char* p = nullptr;

Test t(p);      // t 是 Test<char[]>

推导指引的语法还是简单的,如果只是涉及具体类型,那么只需要:

模板名称(类型a)->模板名称<想要让类型a被推导为的类型>

如果涉及的是一类类型,那么就需要加上 template,然后使用它的模板形参。


我们提一个稍微有点难度的需求:

cpp
template<class Ty, std::size_t size>
struct array {
    Ty arr[size];
};

::array arr{1, 2, 3, 4, 5};     // Error!

类模板 array 同时使用了类型模板形参与非类型模板形参,保有了一个成员是数组。

它无法被我们直接推导出类型,此时就需要我们自己定义推导指引

这会用到我们之前在函数模板里学习到的形参包。

cpp
template<typename T, typename ...Args>
array(T t,Args...) -> array<T, sizeof...(Args) + 1>;

原理很简单,我们要给出 array 的模板类型,那么就让模板形参单独写一个 T 占位,放到形参列表中,并且写一个模板类型形参包用来处理任意个参数;获取 array 的 size 也很简单,直接使用 sizeof... 获取形参包的元素个数,然后再 +1 ,因为先前我们用了一个模板形参占位。

标准库的 std::array 的推导指引,原理和这个一样。

有默认实参的模板形参

和函数模板一样,类模板一样可以有默认实参。

cpp
template<typename T = int>
struct X{};

X x;    // x 是 X<int> C++17 起 OK
X<> x2; // x2 是 X<int>

必须达到 C++17CTAD,才可以在全局、函数作用域声明为 X 这种形式,才能省略 <>

但是在类中声明一个,有默认实参的类模板类型的数据成员(静态或非静态,是否类内定义都无所谓),不管是否达到 C++17,都不能省略 <>

cpp
template<typename T = int>
struct X {};

struct Test{
    X x;                  // Error
    X<> x2;               // OK
    static inline X x3;   // Error
};

但是 gcc13.2 有不同行为,开启 std=c++17,类内定义的静态数据成员省略 <> 可以通过编译。但是,总而言之,不要类内声明中省略 <>

cpp
template<typename T = int>
struct X {};

struct Test{
    static inline X x3;   // OK
};

int main(){
    
}

MinGw clang 16.02msvc 均不可通过编译。

标准库中也经常使用默认实参:

std::vector

cpp
template<
    class T,
    class Allocator = std::allocator<T>
> class vector;

std::string

cpp
template<
    class CharT,
    class Traits = std::char_traits<CharT>,
    class Allocator = std::allocator<CharT>
> class basic_string;

当然了,也可以给非类型模板形参以默认值,虽然不是很常见:

cpp
template<class T, std::size_t N = 10>
struct Arr
{
    T arr[N];
};

Arr<int> x;     // x 是 Arr<int,10> 它保有一个成员 int arr[10]

知道这些即可,这很合理,毕竟函数模板可以,你类模板也可以。

非类型模板形参

前面其实已经提了,像 std::array 都是有非类型模板形参的,这没有什么问题,类似于函数模板。

模板模板形参

类模板的模板类型形参可以接受一个类模板作为参数,我们将它称为:模板模板形参。

先随便给出一个简单的示例:

cpp
template<typename T>
struct X {};

template<template<typename T> typename C>
struct Test {};

Test<X>arr;

模板模板形参的语法略微有些复杂,我们需要理解一下,先把外层的 template<> 去掉。

template<typename T> typename C 我们分两部分看就好

  • 前面的 template<typename T> 就是我们要接受的类模板它的模板列表,是需要一模一样的,比如类模板 X 就是。

  • 后面的 typename 是语法要求,需要声明这个模板模板形参的名字,可以自定义,这样就引入了一个模板模板形参。


下面是详细的语法形式:

txt
template < 形参列表 > typename(C++17)|class 名字(可选)              (1)
template < 形参列表 > typename(C++17)|class 名字(可选) = default    (2)
template < 形参列表 > typename(C++17)|class ... 名字(可选)          (3) (C++11 起)
  1. 可以有名字的模板模板形参

    cpp
    template<typename T>
    struct my_array{
        T arr[10];
    };
    
    template<typename Ty,template<typename T> typename C >
    struct Array {
        C<Ty>array;
    };
    
    Array<int, my_array>arr;    // arr 保有的成员是     my_array<int> 而它保有了 int arr[10]
  2. 有默认模板且可以有名字的模板模板形参

    cpp
    template<typename T>
    struct my_array{
        T arr[10];
    };
    
    template<typename Ty, template<typename T> typename C =  my_array >
    struct Array {
        C<Ty>array;
    };
    
    Array<int>arr;      // arr 的类型同(1),模板模板形参一样可以有    默认值
  3. 可以有名字的模板模板形参包

    其实就是形参包的一种,能接受任意个数的类模板

    cpp
    template<typename T>
    struct X{};
    
    template<typename T>
    struct X2 {};
    
    template<template<typename T>typename...Ts>
    struct Test{};
    
    Test<X, X2, X, X>t;     // 我们可以传递任意个数的模板实参

当然了,模板模板形参也可以和非类型模板形参一起使用,都是一样的,比如:

cpp
template<std::size_t N>
struct X {};

template<template<std::size_t> typename C>
struct Test {};

Test<X>arr;

注意到了吗?我们省略了其中 template<std::size_t> 非类型模板形参的名字,可能通常会写成 template<std::size_t N> ,我们只是为了表达这是可以省略了,看自己的需求。


对于普通的有形参包的类模板也都是同理:

cpp
template<typename... T>
struct my_array{
    int arr[sizeof...(T)];  // 保有的数组大小根据模板类型形参的元素个数
};

template<typename Ty, template<typename... T> typename C = my_array >
struct Array {
    C<Ty>array;
};

Array<int>arr;

成员函数模板

成员函数模板基本上和普通函数模板没多大区别,唯一需要注意的是,它大致有两类:

  • 类模板中的成员函数模板
  • 普通类中的成员函数模板

需要注意的是:

cpp
template<typename T>
struct Class_template{
    void f(T) {}
};

Class_template 的成员函数 f,它不是函数模板,它就是普通的成员函数,在类模板实例化为具体类型的时候,成员函数也被实例化为具体。

  1. 类模板中的成员函数模板

    cpp
    template<typename T>
    struct Class_template{
        template<typename... Args>
        void f(Args&&...args) {}
    };

    f 就是成员函数模板,通常写起来和普通函数模板没多大区别,大部分也都 支持,比如形参包。

  2. 普通类中的成员函数模板

    cpp
    struct Test{
        template<typename...Args>
        void f(Args&&...args){}
    };

    f 就是成员函数模板,没什么问题。


其实都是字面意思,很好理解,上面的示例都没什么实际的使用,都是语法展示,我相信明白函数模板就自然能明白这些。

可变参数类模板

形参包与包展开等知识,在类模板中是通用的。

cpp
template<typename ...Args>
struct X {
    X(Args...args) :value{ args... } {} // 参数展开
    std::tuple<Args...>value;           // 类型形参包展开
};

X x{ 1,"2",'3',4. };    // x 的类型是 X<int,const char*,char,double>
std::cout << std::get<1>(x.value) << '\n'; // 2

std::tuple 是一个模板类,我们用来存储任意类型任意个数的参数,我们指明它的模板实参是使用的模板的类型形参包展开,std::tuple<Args...> 展开后成为 std::tuple<int,const char*,char,double>

构造函数中使用成员初始化列表来初始化成员 value,没什么问题,正常展开。

需要注意的是字符串字面量的类型是 const char[N] ,之所以被推导为 const char* 在于数组之间不能“拷贝”。它隐式转换为了指向数组首地址的指针,类型自然也被推导为 const char*

cpp
int arr[1]{1};
int arr2[2]{1,2};
arr = arr2;           // Error!
cpp
int a = 0;
int b = a;            // OK!

int arr[1]{1};
int arr2[1] = arr;    // Error!
int arr3[1] = {arr};  // Error!

类模板分文件

和前面提到的函数模板分文件的原因一样,类模板也没有办法分文件。

我们给出了一个项目示例,展示类模板通常分文件的情况。

通常就是统一写到 .h 文件中,或者大家约定俗成了一个 .hpp 后缀,这个通常用来放模板。

我们后面会单独做一个内容处理这些情况。

总结

类模板的知识远不止如此,不过目前也足够使用了,后续还会有补充。

我们写的类模板的内容没有函数模板那么多,主要在于很多内容是和函数模板重复的,很多特性彼此之间是相通的,我们就没必要讲那么多,所以需要注意,不要跳着看。