C++ Template: 一些模板相关的棘手内容

整理 C++ 模板中的 typename、初始化、this 指针、成员模板、template 关键字、变量模板和模板模板参数。

本章收集一些模板相关的复杂棘手内容

typename 的必要性

typename 主要是为了区分依赖于 模板参数 的名称是否是 类型,如果 T::SubType 是一个依赖于 T 的名称,编译器默认会将它解释为一个非类型的成员 (如 静态变量枚举值),而不是类型,因此我们必须使用 typename 明确告诉编译器它是一个 类型

例如

template<typename T>
class MyClass {
public:
    void foo() {
        typename T::SubType* ptr; // 明确 SubType 是一个类型
    }
};

这里 T::SubType 依赖于模板参数 T,如果没有 typename,编译器会默认将 T::SubType 视为一个 静态成员(非类型),过 typename T::SubType* ptr; 明确 SubType 是一个类型,使得 ptr 成为指向 T::SubType 类型的 指针

果没有 typename,编译器可能会误将 T::SubType* ptr; 解析成

(T::SubType) * ptr;

T::SubType 作为一个静态成员,乘以 ptr 变量,而不是声明 ptrT::SubType* 类型

T::SubType 不是依赖于模板参数的类型 时,不需要 typename,在非模板代码中,也不需要 typename

class MyType {
public:
    using SubType = int;
};
MyType::SubType x;  // 这里不需要 typename

C++20 之后可能会减少 typename 的使用需求,在某些情况下,编译器可以自动推断 T::TypeName 是类型

初始化 Initialization

默认构造函数 Default Constructor

默认构造函数是 不带参数所有参数都有默认值 的构造函数,它用于在创建对象时初始化对象的成员变量,如果一个类 没有显式定义构造函数,编译器会 自动生成一个默认构造函数

如果没有提供任何构造函数,编译器会自动生成一个无参数的默认构造函数,并执行默认初始化

class A {
    // 没有定义构造函数,编译器会自动生成 A() {}
};
 
int main() {
    A a;  // 调用编译器自动生成的默认构造函数
}

编译器生成的默认构造函数类似于

A() {}  // 默认构造函数

但如果 A非静态成员变量,它们会进行默认初始化

  • 内建类型 (int, double, 指针等): 未定义值 (垃圾值)
  • 类类型:调用其默认构造函数
class A {
public:
    int x;  // 未初始化,可能是垃圾值
};
 
int main() {
    A a;  // x 是未定义的
}

我们可以 手动定义 默认构造函数,以确保对象成员被初始化:

class A {
public:
    int x;
    
    A() {  // 用户定义的默认构造函数
        x = 0;
    }
};

这样,每次创建 A 的对象时,x 都会被初始化为 0

C++11 允许用 = default 明确指定默认构造函数,而不需要手写

class A {
public:
    A() = default;  // 显式声明使用编译器提供的默认构造函数
};

这样,A 依然有 默认构造函数,但避免了手写冗余代码

如果不希望类被默认构造,可以使用 = delete

class A {
public:
    A() = delete;  // 禁止默认构造
};
 
int main() {
    A a;  // ❌ 错误:默认构造函数被禁用
}

这样 A 不能被默认构造,只能通过其他构造函数来创建对象

隐式类型转换

如果我们不希望默认构造函数被隐式调用,可以加上 explicit

class A {
public:
    explicit A() { }  // explicit 限制隐式调用
};
 
void func(A a) {}
 
int main() {
    A a;  // ✅ 允许
    func(A());  // ✅ 允许
    // func({}); // ❌ 错误,不能用 `{}` 隐式调用 explicit 构造函数
}

explicit 关键字禁止某些隐式转换 (如 func({})),适用于 防止误用 的情况

对于 非默认构造函数,也可以使用 explicit 关键字来 防止隐式类型转换,避免意外的错误

在 C++ 中,构造函数可以被隐式调用,例如:

class A {
public:
    A(int n) { }  // 非默认构造函数
};
 
void foo(A a) {}
 
int main() {
    foo(42);  // ✅ 允许,隐式将 42 转换为 A 类型
}

这里 A(int) 构造函数没有 explicit,所以 foo(42) 会隐式地创建 A 类型的对象,这种 隐式转换 有时可能会导致意外的错误

如果我们不希望 int 自动转换为 A,可以加上 explicit

class A {
public:
    explicit A(int n) {}  // 添加 explicit
};
 
void foo(A a) {}
 
int main() {
    foo(42);  // ❌ 错误,不能隐式转换 int -> A
    foo(A(42));  // ✅ 允许,显式创建 A 对象
}

这里 foo(42) 编译错误,因为 explicit 禁止了隐式转换,但 foo(A(42)) 可以编译,因为是 显式调用 构造函数

explicit 对多参数构造函数的影响

如果构造函数有 多个参数,即使没有 explicit,它通常不会被隐式调用

class A {
public:
    A(int n, double d) {}  // 没有 explicit
};
 
void foo(A a) {}
 
int main() {
    foo(10);  // ❌ 错误:无法匹配 A(int, double)
    foo(A(10, 3.14));  // ✅ 允许,显式构造
}

因为 A(int, double) 需要两个参数,编译器不会尝试自动转换 int -> A

但对于 单参数 构造函数,默认允许隐式转换

class A {
public:
    A(int n, double d = 0.0) {}  // 默认参数
};
 
void foo(A a) {}
 
int main() {
    foo(42);  // ✅ 允许,因为 A(int, double) 可以被 int 匹配
}

这里 foo(42) 仍然会发生隐式转换,因为 A(int, double) 只需要一个参数(double 有默认值)

可以用 explicit 禁止这种转换

class A {
public:
    explicit A(int n, double d = 0.0) {}  // 添加 explicit
};
 
void foo(A a) {}
 
int main() {
    foo(42);  // ❌ 错误,不能隐式转换 int -> A
    foo(A(42, 3.14));  // ✅ 允许
}

explicit 影响拷贝初始化

拷贝初始化 Copy Initialization 指的是:

A a = 10;  // 可能会调用 A(int)

如果构造函数是 explicit,则拷贝初始化会被禁止

class A {
public:
    explicit A(int n) {}
};
 
int main() {
    A a = 10;  // ❌ 错误,explicit 禁止拷贝初始化
    A b(10);   // ✅ 允许,直接初始化
}

A a = 10; 编译错误(拷贝初始化被 explicit 禁止), A b(10); 可以编译(直接初始化 A(10)

初始化对象

在 C++ 中,有多种方式可以初始化一个对象

  1. 拷贝初始化 Copy Initialization

    T x = T();  // 拷贝初始化

    这种初始化方式通常会调用 T默认构造函数,然后再通过 拷贝构造函数移动构造函数 来初始化 x,但如果 T 的构造函数是 explicit 的,拷贝初始化 是不允许的

  2. 直接初始化 Direct Initialization

    T x{};      // 直接初始化(C++11 之后的统一初始化)
    T x = {};   // 也是直接初始化

    这种方式会直接调用 T默认构造函数(如果存在), 如果 T 是内建类型,则 x 进行零初始化

explicit 构造函数的限制 (C++17 之前)

假设有一个 explicit 默认构造函数

struct A {
    explicit A() {}  // 显式构造函数
};

在 C++11/14 中,以下代码是错误的

A a = A();  // ❌ 错误:拷贝初始化不允许 explicit 构造函数

因为 explicit 的构造函数不能用于 拷贝初始化,只能用于 直接初始化

A a{};      // ✅ 直接初始化,可以调用 explicit 构造函数
A a = {};   // ✅ 也可以

C++17 之后的 强制性拷贝消除

在 C++17 之前,T x = T(); 可能会涉及拷贝或移动构造函数,但 C++17 引入了 强制性拷贝消除 Mandatory Copy Elision 使得 T x = T();T x{}; 的效果完全一致

也就是说,在 C++17 之后

T x = T();  // ✅ 在 C++17 中等价于 T x{};

编译器不会再调用拷贝构造函数或移动构造函数,而是直接构造 T

因此,即使 T 只有一个 explicit 默认构造函数,在 C++17 之后,T x = T(); 也是合法的

struct A {
    explicit A() {}  
};
 
A a = A();  // ✅ 在 C++17 之后合法

由于 T x{}; 方式一直可用,并且不会受到 explicit 的影响,因此 推荐使用 T x{};,避免潜在的问题:

T x{};  // 推荐的方式

零初始化 Zero Initialization

在 C++ 中,内建类型的变量 如果不初始化,它们的值是不确定的

void foo()
{
    int x;      // x 的值是未定义的
    int* ptr;   // ptr 指向未知地址(不是 nullptr)
}

这会导致潜在的 未定义行为 UB

类似地,如果我们在模板中声明一个变量

template<typename T>
void foo()
{
    T x; // 如果 T 是内建类型,则 x 可能是未初始化的
}

如果 Tint,那么 x 也是未定义的

为了解决这个问题,可以 显式 地使用 值初始化 Value Initialization

template<typename T>
void foo()
{
    T x{};  // 如果 T 是内建类型,则 x 被初始化为 0(或 false/nullptr)
}

值初始化的行为

  • 内建类型(如 intdoublebool、指针):被 零初始化(0falsenullptr
  • 类类型(struct/class):调用 默认构造函数(如果存在)
foo<int>();     // x 被初始化为 0
foo<double>();  // x 被初始化为 0.0
foo<bool>();    // x 被初始化为 false
foo<int*>();    // x 被初始化为 nullptr

在 C++17 之后,强制性拷贝消除 使得 T x = T();T x{}; 作用相同

类模板中使用零初始化

类模板 中,如果成员变量的类型是模板参数,我们也需要确保它被正确初始化

template<typename T>
class MyClass {
private:
    T x;
public:
    MyClass() : x{} {}  // 确保 x 被零初始化
};

C++11 之后,我们可以直接在成员变量声明时初始化

template<typename T>
class MyClass {
private:
    T x{};  // 零初始化 x(如果 T 是内建类型)
};

默认参数的零初始化

默认参数不能使用 {} 进行零初始化

template<typename T>
void foo(T p{}) {  // ❌ 错误!
    ...
}

正确的方式(C++11 之前需要 T()

template<typename T>
void foo(T p = T{}) {  // ✅ OK
    ...
}

模板中的 this 指针

在模板编程中,如果 基类 是一个依赖于 模板参数 的类模板,那么在 派生类 中访问基类的成员时,可能会遇到 未解析的标识符 问题

template<typename T>
class Base {
public:
    void bar() {}
};
 
template<typename T>
class Derived : Base<T> { // 继承 Base<T>
public:
    void foo() {
        bar();  // ❌ 可能报错
    }
};

foo() 中,bar(); 不会解析到 Base<T>::bar(),可能会导致

  1. 编译错误(找不到 bar
  2. 调用全局的 bar() (如果有相同名字的全局函数)

因为 Base<T> 是一个 依赖类型 dependent type, 它依赖于 T, 在 C++ 的 两阶段解析 two-phase lookup 中,编译器在第一阶段解析 foo() 时,不会自动在 Base<T> 里查找 bar()

我们可以使用 this-> 访问基类成员

void foo() {
    this->bar();  // ✅ 让编译器知道 bar() 来自基类
}

或者使用 Base<T>:: 访问基类成员

void foo() {
    Base<T>::bar();  // ✅ 直接指定基类作用域
}

在 C++ 中,this-> 代表当前实例的指针,显式使用 this->bar(); 可以告诉编译器

  • bar 是一个成员函数,而不是一个独立的全局函数
  • bar 来自 Base<T>,它是一个依赖类型,所以要等模板实例化时再解析
template<typename T>
class Base {
public:
    void bar() {}
};
 
template<typename T>
class Derived : Base<T> {
public:
    void foo() {
        this->bar();  // ✅ 显式使用 this->
    }
};

另一种解决方案是直接使用基类的作用域

void foo() {
    Base<T>::bar();  // ✅ 直接指定基类作用域
}

这种方式也可以让编译器在实例化时正确解析 bar

template<typename T>
class Base {
public:
    void bar() {}
};
 
template<typename T>
class Derived : Base<T> {
public:
    void foo() {
        Base<T>::bar();  // ✅ 直接使用基类作用域
    }
};

如果 bar() 是静态成员函数,则只能使用 Base<T>::bar();

template<typename T>
class Base {
public:
    static void bar() {}
};
 
template<typename T>
class Derived : Base<T> {
public:
    void foo() {
        Base<T>::bar();  // ✅ 访问静态成员
        // this->bar();  // ❌ 错误,静态成员不属于 this 指针
    }
};

如果基类有 typedefusing,在子类中使用时也需要 this->Base<T>::

template<typename T>
class Base {
public:
    using Type = int;
};
 
template<typename T>
class Derived : Base<T> {
public:
    void foo() {
        typename Base<T>::Type var = 10;  // ✅ 正确
    }
};

这里 typename 也是必须的,因为 Base<T>::Type 是类型依赖项

模板与原始数组及字符串字面量

在 C++ 模板编程中,原始数组 raw arrays字符串字面量 string literals 作为模板参数时会有一些特殊的规则,尤其是在传递数组时的 类型衰退 decay 以及数组大小的推导等问题

传递数组或字符串字面量时的类型衰退

在 C++ 中,数组变量通常会 衰退 decay 为指针,除非以引用方式传递

void foo(int arr[10]) {
    std::cout << sizeof(arr) << '\n';  // 输出指针大小,而非数组大小
}

上面 arr[10] 只是语法上的写法,实际上 arrfoo 内部被当作 int* 处理

但如果 模板参数是引用类型,数组就不会衰退

template<typename T>
void bar(T& arr) {
    std::cout << sizeof(arr) << '\n';  // 输出完整数组的大小
}
 
int main() {
    int x[10];
    bar(x);  // 这里 arr 仍然是 int[10]
}

所以,在模板编程中,如果要保持数组的类型信息,应该使用 T(&)[N],这样可以在模板参数推导时保留数组的大小信息

使用模板处理不同长度的数组

在一些情况下,我们希望模板函数能够正确处理不同长度的数组

template<typename T, int N, int M>
bool less(T (&a)[N], T (&b)[M]) {
    for (int i = 0; i < N && i < M; ++i) {
        if (a[i] < b[i]) return true;
        if (b[i] < a[i]) return false;
    }
    return N < M;
}
 
int main() {
    int x[] = {1, 2, 3};
    int y[] = {1, 2, 3, 4, 5};
    std::cout << less(x, y) << '\n';  // N=3, M=5
}

T(&a)[N] 使 a 变成数组引用,从而保留数组大小 N, 模板参数 NM 由数组大小推导,使得 less() 函数可以比较任意长度的数组

处理字符串字面量

字符串字面量的类型是 const char[N],在函数模板中使用时也可以保留其长度

std::cout << less("ab", "abc") << '\n';

less() 模板中, "ab" 的类型是 const char[3](包括 \0), "abc" 的类型是 const char[4](包括 \0), 这样,less<>() 会被实例化为 less<const char, 3, 4>

如果只希望支持字符串字面量,可以使用

template<int N, int M>
bool less(char const (&a)[N], char const (&b)[M]) {
    for (int i = 0; i < N && i < M; ++i) {
        if (a[i] < b[i]) return true;
        if (b[i] < a[i]) return false;
    }
    return N < M;
}

这样 less() 只适用于 char 数组,而不适用于 int 数组

模板特化处理不同类型的数组

有时,我们需要根据数组的不同情况提供不同的处理方式

template<typename T>
struct MyClass;  // 主模板(不会被实例化)
 
// 处理已知大小的数组
template<typename T, std::size_t SZ>
struct MyClass<T[SZ]> {
    static void print() { std::cout << "print() for T[" << SZ << "]\n"; }
};
 
// 处理引用数组(已知大小)
template<typename T, std::size_t SZ>
struct MyClass<T(&)[SZ]> {
    static void print() { std::cout << "print() for T(&)[" << SZ << "]\n"; }
};
 
// 处理未知大小的数组
template<typename T>
struct MyClass<T[]> {
    static void print() { std::cout << "print() for T[]\n"; }
};
 
// 处理未知大小数组的引用
template<typename T>
struct MyClass<T(&)[]> {
    static void print() { std::cout << "print() for T(&)[]\n"; }
};
 
// 处理指针
template<typename T>
struct MyClass<T*> {
    static void print() { std::cout << "print() for T*\n"; }
};

main() 函数中,我们可以测试不同情况

int main() {
    int a[42];  
    MyClass<decltype(a)>::print();  // 调用 MyClass<T[SZ]>
 
    extern int x[];  // 未知大小的数组
    MyClass<decltype(x)>::print();  // 调用 MyClass<T[]>
}
int x[] = {0, 8, 15};  // 定义前面声明的数组

如果需要在模板函数内部获取参数类型,可以使用 decltype

template<typename T1, typename T2, typename T3>
void foo(int a1[7], int a2[], int (&a3)[42], int (&x0)[], T1 x1, T2& x2, T3&& x3) {
    MyClass<decltype(a1)>::print();  // MyClass<T*>,数组退化为指针
    MyClass<decltype(a2)>::print();  // MyClass<T*>,数组退化为指针
    MyClass<decltype(a3)>::print();  // MyClass<T(&)[SZ]>,引用保留大小
    MyClass<decltype(x0)>::print();  // MyClass<T(&)[]>,未知大小数组的引用
    MyClass<decltype(x1)>::print();  // MyClass<T*>,数组退化为指针
    MyClass<decltype(x2)>::print();  // MyClass<T(&)[]>,未知大小数组的引用
    MyClass<decltype(x3)>::print();  // MyClass<T(&)[]>,未知大小数组的引用
}
 
int main() {
    int a[42];
    MyClass<decltype(a)>::print();  // 使用 MyClass<T[SZ]>
 
    extern int x[];
    MyClass<decltype(x)>::print();  // 使用 MyClass<T[]>
    
    foo(a, a, a, x, x, x, x);
}
  • 数组作为函数参数会退化为指针,所以 a1a2 变成 T*
  • 引用数组不会退化,所以 a3 仍然是 T(&)[42]
  • 未知大小的数组 x 仍然保留 T[] 类型,可以用于不完整类型

成员模板 Member Templates

在 C++ 中,类的成员(包括 成员函数嵌套类)也可以是模板,这种特性允许我们在 类模板 中定义 成员函数模板嵌套类模板,从而支持不同类型的操作,而不改变整个类模板的参数

考虑一个 Stack 模板类

template<typename T>
class Stack {
private:
    std::deque<T> elems;  // 使用 deque 作为底层容器
public:
    void push(T const& elem) { elems.push_back(elem); }
    void pop() { elems.pop_back(); }
    T const& top() const { return elems.back(); }
    bool empty() const { return elems.empty(); }
};

如果 Stack<int>Stack<float> 之间想要进行赋值(即 floatStack = intStack;),默认的赋值操作符不支持 不同类型 的栈相互赋值, 默认的赋值操作要求左右两侧的类型完全相同

Stack<int> intStack;
Stack<float> floatStack;
floatStack = intStack;  // ❌ 编译错误,类型不匹配

我们可以定义一个成员模板赋值操作符,使得 Stack<T> 可以接收 Stack<T2> 作为赋值对象

template<typename T>
class Stack {
private:
    std::deque<T> elems;
public:
    void push(T const& elem) { elems.push_back(elem); }
    void pop() { elems.pop_back(); }
    T const& top() const { return elems.back(); }
    bool empty() const { return elems.empty(); }
 
    // 成员模板:支持不同类型的 Stack 赋值
    template<typename T2>
    Stack& operator= (Stack<T2> const& op2);
};

实现 operator=

template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator= (Stack<T2> const& op2) {
    elems.clear();  // 清空当前栈
    Stack<T2> tmp(op2);  // 复制要赋值的栈 (因为是引用,我们不想影响传入的 Stack)
    while (!tmp.empty()) {
        elems.push_front(tmp.top());  // 依次取出元素
        tmp.pop();
    }
    return *this;
}

这样,我们可以赋值不同类型的 Stack

Stack<int> intStack;
Stack<float> floatStack;
floatStack = intStack;  // ✅ 允许,int 可以隐式转换为 float

这里需要注意的是,在 Stack<T> 里,elemsstd::deque<T>,它存储的是 T 类型的数据, 因此,在 operator= 赋值时

elems.push_front(tmp.top());

tmp.top() 的返回值是 T2, push_front() 需要 T, 如果 T2 可以转换为 T(如 intfloat),赋值将成功, 如果 T2 不能转换 为 T(如 std::stringfloat),编译将报错

Stack<std::string> stringStack;
Stack<float> floatStack;
floatStack = stringStack;  // ❌ 编译错误,string 不能转换为 float

友元模板 Friend Templates

我们之所以要用这么迂回的办法来复制不同类型的 Stack,是因为我们无法在不同类型的模版之间互访

如果 Stack<T2> 需要访问 Stack<T>private 成员,我们可以声明它们为 友元

template<typename T>
class Stack {
private:
    std::deque<T> elems;
public:
    template<typename T2>
    Stack& operator= (Stack<T2> const&);
 
    template<typename> friend class Stack;  // 允许不同类型的 Stack 互相访问私有成员
};

这样,Stack<int>Stack<float> 互相赋值时,Stack<float> 仍然可以访问 Stack<int>elems

那么我们就可以这样实现拷贝赋值

template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator= (Stack<T2> const& op2)
{
    elems.clear(); // remove existing elements
    elems.insert(
        elems.begin(), // insert at the beginning
        op2.elems.begin(), // all elements from op2
        op2.elems.end()
    );
    return *this;
}

当然这不会改变 op2 的类型,所以如果不是可以自动转换的目标类型,编译仍然会失败

泛化 Stack 的内部容器

在上面的 Stack<T> 实现中,我们硬编码了 std::deque<T> 作为底层容器,我们可以让用户自定义容器

template<typename T, typename Cont = std::deque<T>>
class Stack {
private:
    Cont elems;
public:
    void push(T const& elem) { elems.push_back(elem); }
    void pop() { elems.pop_back(); }
    T const& top() const { return elems.back(); }
    bool empty() const { return elems.empty(); }
 
    template<typename T2, typename Cont2>
    Stack& operator= (Stack<T2, Cont2> const&);
};

这样,我们可以使用 std::vector 作为底层容器

Stack<int, std::vector<int>> vStack;
vStack.push(42);
vStack.push(7);
std::cout << vStack.top() << '\n';  // 输出 7

成员模板的特化

成员模板也可以 完全特化部分特化

class BoolString {
private:
    std::string value;
public:
    BoolString(std::string const& s) : value(s) {}
 
    // 成员模板
    template<typename T = std::string>
    T get() const {
        return value;
    }
};
 
// 对 `get<bool>()` 进行完全特化
template<>
inline bool BoolString::get<bool>() const {
    return value == "true" || value == "1" || value == "on";
}

例如

BoolString s1("hello");
std::cout << s1.get() << '\n';       // 输出 "hello"
std::cout << s1.get<bool>() << '\n'; // 输出 "false"
 
BoolString s2("on");
std::cout << s2.get<bool>() << '\n'; // 输出 "true"

s1.get() 默认返回 std::strings1.get<bool>() 调用特化版本,判断 value 是否是 "true""1""on"

成员模板不会替代特殊成员函数

模板成员函数 不会替代默认的 拷贝构造函数移动构造函数,如果相同类型的 Stack<T> 进行赋值,仍然会调用默认的拷贝赋值运算符,而不是模板版本

Stack<int> s1, s2;
s2 = s1;  // 仍然调用默认的拷贝赋值运算符,而不是模板 operator=

template 关键字

在 C++ 模板编程中,有时需要显式告诉编译器,后面的 < 是模板参数列表的开始,而不是小于号,这时,就需要使用 .template 关键字

考虑下面的 std::bitset 例子

#include <bitset>
#include <iostream>
 
template<unsigned long N>
void printBitset(std::bitset<N> const& bs) {
    std::cout << 
        bs.template to_string
        <
            char, 
            std::char_traits<char>, 
            std::allocator<char>
        >() 
    << std::endl;
}

bs.to_string<char, std::char_traits<char>, std::allocator<char>>()

  • bsstd::bitset<N>,它的类型依赖于模板参数 N(即 std::bitset<32>std::bitset<64> 等)
  • to_string()std::bitset<N> 的一个成员模板函数

如果去掉 template 关键字

std::cout << bs.to_string<char, std::char_traits<char>, std::allocator<char>>();

会导致编译错误

error: expected primary-expression before ‘char
  • 编译器无法判断 < 是小于号还是模板参数列表的开始,编译器默认认为 < 是小于号,所以报错
  • bs 是依赖模板参数 N 的类型,它的 to_string 可能是普通成员函数,也可能是成员模板

所以我们需要使用 .template 显式告诉编译器,to_string 是一个成员模板

.template 的适用场景

  1. 调用成员模板
    只要一个 依赖模板参数的对象 调用了 成员模板函数,就需要 template

    template<typename T>
    struct Wrapper {
        template<typename U>
        void func(U val) {
            std::cout << "Value: " << val << std::endl;
        }
    };
     
    template<typename T>
    void callFunc(Wrapper<T>& w) {
        w.template func<int>(42);  // ✅ 正确,显式指定 func<int>
    }

    错误示例(缺少 .template

    void callFunc(Wrapper<T>& w) {
        w.func<int>(42);  // ❌ 编译错误,编译器认为 < 是小于号
    }
  2. 调用基类的成员模板

    template<typename T>
    class Base {
    public:
        template<typename U>
        void func(U val) {
            std::cout << "Base::func called with " << val << std::endl;
        }
    };
     
    template<typename T>
    class Derived : public Base<T> {
    public:
        void callBaseFunc() {
            this->template func<int>(42);  // ✅ 必须加 template
        }
    };

    Base<T>::func<U>() 是一个 成员模板Base<T> 依赖于 T,编译器在解析 func<int> 时,不知道 func 是模板,默认假设 func<int>this 小于 int,所以报错,需要 this->template func<int>(42); 显式指明 func 是模板函数

    错误示例(缺少 .template

    this->func<int>(42);  // ❌ 编译错误
  3. 使用 ->template 调用模板
    当对象是指针时,需要 ->template

    template<typename T>
    class Container {
    public:
        template<typename U>
        void method(U val) {
            std::cout << "Value: " << val << std::endl;
        }
    };
     
    template<typename T>
    void callMethod(Container<T>* c) {
        c->template method<int>(10);  // ✅ 必须加 template
    }

    错误示例(缺少 ->template

    c->method<int>(10);  // ❌ 编译错误
  4. 使用 ::template 访问嵌套模板
    当访问嵌套类模板或静态成员模板时,需要 ::template

    template<typename T>
    struct Outer {
        template<typename U>
        struct Inner {
            static void print() {
                std::cout << "Inner template called\n";
            }
        };
    };
     
    template<typename T>
    void callInner() {
        Outer<T>::template Inner<int>::print();  // ✅ 必须加   template
    }

    错误示例(缺少 ::template

    Outer<T>::Inner<int>::print();  // ❌ 编译错误

注意 .template 不是全局适用的,.template 只能用于

  1. 模板代码内部
  2. ., ->, :: 后面
  3. 前面部分是依赖模板参数的表达式

如果 .template 用在非模板环境,会导致错误

std::vector<int> v;
v.template push_back(42);  // ❌ 编译错误,不是依赖模板参数的情况

泛型 Lambda 与成员模板

C++14 引入的 泛型 Lambda (Generic Lambda),本质上是 成员模板 Member Template 的快捷方式,这意味着,一个使用 auto 作为参数类型的 Lambda 表达式,等价于一个带有 operator() 成员模板的类

一个普通的泛型 Lambda

auto sum = [] (auto x, auto y) {
    return x + y;
};

等价于编译器生成的如下 类模板

class SomeCompilerSpecificName {
public:
    SomeCompilerSpecificName() = default;  // 仅编译器可调用的默认构造函数
    
    template<typename T1, typename T2>
    auto operator() (T1 x, T2 y) const {
        return x + y;
    }
};

等价使用

SomeCompilerSpecificName sum;
std::cout << sum(3, 4.5) << std::endl;  // 7.5
std::cout << sum(std::string("Hello "), "World") << std::endl;  // Hello World

泛型 Lambda 等价于一个 默认构造的匿名类(闭包对象),该类的 operator() 是一个 成员模板,支持任意类型参数,这样,我们可以用 同一个 Lambda 处理不同类型的数据(如 intdoublestd::string 等)

如果 Lambda 捕获变量,等价类会有额外的成员变量

int a = 10;
auto lambda = [a](auto x) {
    return a + x;
};

这等价于

class SomeCompilerSpecificName {
private:
    int a;
public:
    SomeCompilerSpecificName(int a) : a(a) {}  // 存储捕获的 a
 
    template<typename T>
    auto operator()(T x) const {
        return a + x;
    }
};

调用

auto lambda = [a](auto x) { return a + x; };
std::cout << lambda(5) << std::endl;  // 15

等价于

SomeCompilerSpecificName obj(10);
std::cout << obj(5) << std::endl;  // 15

变量模板 Variable Templates

C++14 引入了 变量模板 Variable Templates,使 变量 也可以像函数模板和类模板一样按类型进行参数化,变量模板在某些情况下可以提高代码可读性和复用性,尤其是 数学常量全局配置静态成员变量类型特性 type traits 相关的场景

一个典型的变量模板示例

template<typename T>
constexpr T pi = T{3.1415926535897932385};

这个 pi 不是一个普通变量,而是一个 模板变量,它可以用不同的 类型 实例化

std::cout << pi<double> << '\n';  // 3.14159 (double)
std::cout << pi<float> << '\n';   // 3.14159 (float)
std::cout << pi<long double> << '\n';  // 3.14159 (long double)

注意,必须加尖括号 <> 来指定类型,否则会 编译错误

std::cout << pi << '\n';  // ❌ 错误,必须指定 `pi<>`

变量模板 不能在函数或块作用域内定义,只能定义在全局或命名空间

可以给变量模板提供 默认模板参数

template<typename T = long double>
constexpr T pi = T{3.1415926535897932385};
 
std::cout << pi<> << '\n';  // ✅ long double
std::cout << pi<float> << '\n';  // ✅ float

如果不指定 <>,就默认使用 long double

变量模板的跨翻译单元 Translation Unit

变量模板可以在多个 翻译单元(TU)中使用,例如

header.hpp

template<typename T> T val{};  // 变量模板,默认初始化为 0

文件1(translation unit 1)

#include "header.hpp"
 
void print();
 
int main() {
    val<long> = 42;  // 设置 `val<long>` 的值
    print();
}

文件2(translation unit 2)

#include "header.hpp"
#include <iostream>
 
void print() {
    std::cout << val<long> << '\n';  // ✅ 输出 42
}

即使 val<long>不同的翻译单元 中被访问,它们仍然指向同一个变量

变量模板支持非类型模板参数

变量模板不仅支持 类型参数,还支持 非类型模板参数(如 intchar、指针等)

#include <array>
#include <iostream>
 
template<int N>
std::array<int, N> arr{};  // N 长度的数组,默认初始化为 0
 
int main() {
    arr<10>[0] = 42;
    for (int i = 0; i < 10; ++i) {
        std::cout << arr<10>[i] << ' ';  // 42 0 0 0 0 0 0 0 0 0
    }
}

arr<10>std::array<int, 10>,在所有 翻译单元 中共享,arr<20>std::array<int, 20>,是一个 不同的变量

自动推导类型的变量模板

template<auto N>
constexpr decltype(N) dval = N;  // 变量类型取决于 N
 
int main() {
    std::cout << dval<'A'> << '\n';  // 输出 'A' (char)
    std::cout << dval<42> << '\n';   // 输出 42 (int)
}

dval<'A'> 的类型是 chardval<42> 的类型是 intdecltype(N) 自动推导类型,可以避免冗长的模板参数声明

变量模板用于类的静态成员

变量模板 可以作为 类模板的静态成员,让访问更加直观

template<typename T>
class MyClass {
public:
    static constexpr int max = 1000;
};
 
// 变量模板映射类成员
template<typename T>
constexpr int myMax = MyClass<T>::max;
 
// 直接使用变量模板
int main() {
    std::cout << myMax<std::string> << '\n';  // 1000
}

这样,程序员可以写

auto i = myMax<std::string>;

而不是

auto i = MyClass<std::string>::max;

这种用法在 标准库 中大量使用,例如 std::numeric_limits

#include <limits>
#include <iostream>
 
// 变量模板简化访问
template<typename T>
constexpr bool isSigned = std::numeric_limits<T>::is_signed;
 
int main() {
    std::cout << isSigned<int> << '\n';   // 1 (true)
    std::cout << isSigned<unsigned> << '\n'; // 0 (false)
}

这样可以写

isSigned<char>

而不是

std::numeric_limits<char>::is_signed

还有 C++17 的 type_traits 提供 _v 变量模板 C++17 之前,我们使用 std::is_const<T>::value 检查 T 是否是 const

std::is_const<int>::value  // 0
std::is_const<const int>::value  // 1

C++17 提供了 变量模板 _v,可以写成

std::is_const_v<int>  // 0
std::is_const_v<const int>  // 1

C++17 的 _v 变量模板

namespace std {
    template<typename T>
    constexpr bool is_const_v = is_const<T>::value;
}

还有许多其他类似的 _v 的变量模板

模板作为模板参数 Template Template Parameters

在 C++ 中,模板不仅可以接受 类型参数typenameclass),还可以接受 模板本身作为参数, 这就是 模板模板参数 Template Template Parameter

在不使用模板模板参数的情况下,我们定义 Stack 时,用户必须指定 容器类型元素类型

Stack<int, std::vector<int>> vStack;  // int 类型的栈,底层使用 vector

这导致 类型冗余int 作为 Stack 的元素类型已经指定过了,std::vector<int> 再次重复 int

如果 Stack 允许只指定容器模板,而不重复指定 int,可以这样写

Stack<int, std::vector> vStack;  // int 类型的栈,底层使用 vector

这样 Stack底层容器自动推导元素类型,简化了代码

基本写法是

template<typename T, template<typename> class Cont = std::deque>
class Stack {
private:
    Cont<T> elems;  // 用 Cont<T> 作为底层容器
public:
    void push(T const& elem) { elems.push_back(elem); }
    void pop() { elems.pop_back(); }
    T const& top() const { return elems.back(); }
    bool empty() const { return elems.empty(); }
};
  • template<typename T, template<typename> class Cont>

    • TStack 存储的元素类型
    • Cont一个类模板,它必须是 一个接收单个类型参数的类模板(如 std::vector<T>std::deque<T>
  • 默认值 std::deque
    Cont = std::deque 使 Stack 默认使用 std::deque 作为底层容器

  • 成员变量 Cont<T> elems;
    Cont<T> 实例化 Cont,并使用 T 作为其元素类型,比如 std::vector<int>

这样 Stack 就可以支持不同的容器

Stack<int> dStack;          // 默认使用 std::deque<int>
Stack<int, std::vector> vStack;  // 使用 std::vector<int>

这等价于

Stack<int, std::deque<int>> dStack;
Stack<int, std::vector<int>> vStack;

不用重复指定 int,简化了代码

使用模板模板参数定义成员函数

成员函数也需要正确声明 模板模板参数

template<typename T, template<typename> class Cont>
void Stack<T, Cont>::push(T const& elem) {
    elems.push_back(elem);
}

完整示例

#include <iostream>
#include <vector>
#include <deque>
 
template<typename T, template<typename> class Cont = std::deque>
class Stack {
private:
    Cont<T> elems;
public:
    void push(T const& elem) { elems.push_back(elem); }
    void pop() { elems.pop_back(); }
    T const& top() const { return elems.back(); }
    bool empty() const { return elems.empty(); }
};
 
int main() {
    Stack<int> dStack;          // 默认使用 std::deque<int>
    Stack<int, std::vector> vStack;  // 使用 std::vector<int>
 
    dStack.push(10);
    dStack.push(20);
    std::cout << "dStack.top(): " << dStack.top() << '\n';  // 输出 20
 
    vStack.push(100);
    vStack.push(200);
    std::cout << "vStack.top(): " << vStack.top() << '\n';  // 输出 200
}

C++17 之前的兼容性问题

在 C++17 之前,模板模板参数必须完全匹配,即

template
<
    typename T,
    template
    <
        typename Elem, 
        typename Alloc = std::allocator<Elem>
    >
    class Cont = std::deque
>
class Stack {
private:
    Cont<T> elems;
};

typename Alloc = std::allocator<Elem> 是必须的,因为 std::deque<T> 实际上有两个模板参数

template <typename T, typename Allocator = std::allocator<T>>
class deque;

C++17 之前,模板模板参数必须完全匹配,即 Cont<T> 需要接受两个模板参数

C++17 允许省略未使用的模板参数

template<typename T,
         template<typename> typename Cont = std::deque>  // ✅ C++17 才允许 typename
class Stack {
private:
    Cont<T> elems;
};

C++14 及更早版本不能这样写,因为 typename 不能用于模板模板参数

友元声明中的模板模板参数

为了让不同 Stack<T> 访问彼此的 private 成员,我们使用了 友元模板,让我们不要忘了为它也加上 模板模板参数

template<typename, template<typename> class>
friend class Stack;

完整示例

template<typename T, template<typename> class Cont = std::deque>
class Stack {
private:
    Cont<T> elems;
public:
    template<typename, template<typename> class>
    friend class Stack;
};

这样 Stack<int, std::vector>Stack<float, std::vector> 可以互相访问 private 成员

std::array 不能用作 Cont

std::array 不能用于 Stack

Stack<int, std::array> s;  // ❌ 编译错误

std::array<T, N> 有两个模板参数(TN), Stack 期望 Cont<T> 只有一个模板参数,所以 std::array 不兼容

可以定义 特化 版本来兼容它

template<typename T, std::size_t N>
class Stack<T, std::array<T, N>> {  // ✅ 针对 std::array 的特化
private:
    std::array<T, N> elems;
};