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 变量,而不是声明 ptr 为 T::SubType* 类型
当 T::SubType 不是依赖于模板参数的类型 时,不需要 typename,在非模板代码中,也不需要 typename
class MyType {
public:
using SubType = int;
};
MyType::SubType x; // 这里不需要 typenameC++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++ 中,有多种方式可以初始化一个对象
-
拷贝初始化 Copy Initialization
T x = T(); // 拷贝初始化这种初始化方式通常会调用
T的 默认构造函数,然后再通过 拷贝构造函数 或 移动构造函数 来初始化x,但如果T的构造函数是explicit的,拷贝初始化 是不允许的 -
直接初始化 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 可能是未初始化的
}如果 T 是 int,那么 x 也是未定义的
为了解决这个问题,可以 显式 地使用 值初始化 Value Initialization
template<typename T>
void foo()
{
T x{}; // 如果 T 是内建类型,则 x 被初始化为 0(或 false/nullptr)
}值初始化的行为
- 内建类型(如
int、double、bool、指针):被 零初始化(0、false、nullptr) - 类类型(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(),可能会导致
- 编译错误(找不到
bar) - 调用全局的
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 指针
}
};如果基类有 typedef 或 using,在子类中使用时也需要 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] 只是语法上的写法,实际上 arr 在 foo 内部被当作 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, 模板参数 N 和 M 由数组大小推导,使得 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);
}- 数组作为函数参数会退化为指针,所以
a1和a2变成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> 里,elems 是 std::deque<T>,它存储的是 T 类型的数据, 因此,在 operator= 赋值时
elems.push_front(tmp.top());tmp.top() 的返回值是 T2, push_front() 需要 T, 如果 T2 可以转换为 T(如 int → float),赋值将成功, 如果 T2 不能转换 为 T(如 std::string → float),编译将报错
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::string,s1.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>>()
bs是std::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 的适用场景
-
调用成员模板
只要一个 依赖模板参数的对象 调用了 成员模板函数,就需要templatetemplate<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); // ❌ 编译错误,编译器认为 < 是小于号 } -
调用基类的成员模板
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); // ❌ 编译错误 -
使用
->template调用模板
当对象是指针时,需要->templatetemplate<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); // ❌ 编译错误 -
使用 ::template 访问嵌套模板
当访问嵌套类模板或静态成员模板时,需要::templatetemplate<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 只能用于
- 模板代码内部
.,->,::后面- 前面部分是依赖模板参数的表达式
如果 .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 处理不同类型的数据(如 int、double、std::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> 在 不同的翻译单元 中被访问,它们仍然指向同一个变量
变量模板支持非类型模板参数
变量模板不仅支持 类型参数,还支持 非类型模板参数(如 int、char、指针等)
#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'> 的类型是 char,dval<42> 的类型是 int,decltype(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 // 1C++17 提供了 变量模板 _v,可以写成
std::is_const_v<int> // 0
std::is_const_v<const int> // 1C++17 的 _v 变量模板
namespace std {
template<typename T>
constexpr bool is_const_v = is_const<T>::value;
}还有许多其他类似的 _v 的变量模板
模板作为模板参数 Template Template Parameters
在 C++ 中,模板不仅可以接受 类型参数(typename 或 class),还可以接受 模板本身作为参数, 这就是 模板模板参数 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>T是Stack存储的元素类型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> 有两个模板参数(T 和 N), 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;
};