# 类模板

本节将介绍类模板

## 初识类模板

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

### 定义类模板

下面是一个类模板，它和普通类的区别只是多了一个 `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>` 我们稍微带入一下，模板的 `T` 是 `void` 那 `T 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`](https://zh.cppreference.com/w/cpp/container/array/deduction_guides) 的推导指引，原理和这个一样。

## 有默认实参的模板形参

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

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

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

必须达到 **C++17** 有 [`CTAD`](https://zh.cppreference.com/w/cpp/language/class_template_argument_deduction)，才可以在全局、函数作用域声明为 `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 有[不同行为](https://godbolt.org/z/n1EfWf9GM)，开启 `std=c++17`，类内定义的静态数据成员省略 `<>` 可以通过编译。但是，总而言之，不要类内声明中省略 `<>`。

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

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

int main(){
    
}
```

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

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

[`std::vector`](https://zh.cppreference.com/w/cpp/container/vector)

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

[`std::string`](https://zh.cppreference.com/w/cpp/string/basic_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`](https://zh.cppreference.com/w/cpp/utility/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!
```

## 类模板分文件

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

我们给出了一个[项目示例](https://github.com/Mq-b/Modern-Cpp-templates-tutorial/blob/main/code/03类模板分文件/README.md)，展示类模板通常分文件的情况。

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

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

## 总结

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

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://mq-bai.gitbook.io/modern-cpp-templates-cookbook/02-lei-mu-ban.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
