C++模板梳理
目录
函数模板
有默认实参的模板类型形参
非类型模板形参
重载函数模板
可变参数模板
类模板
定义类模板
类模板参数推导
用户定义的推导指引
有默认实参的模板形参
非类型模板形参
模板模板形参
成员函数模板
可变参数类模板
类静态数据成员
变量模板
有默认实参的模板形参
非类型模板形参
模板全特化
函数模板全特化
类模板全特化
变量模板全特化
实现细节
模板偏特化
函数模板
类模板偏特化
变量模板偏特化
SFINAE
基础使用示例
标准库支持
std::enable_if
std::void_t
std::declval
Type Traits
Type Traits 的核心思想
1. 类型分析(Type Inspection)
2. 类型转换(Type Transformation)
3. 条件模板逻辑(SFINAE / Concepts)
实现机制
与 Concepts、requires 的关系
总结
模板显式实例化解决文件分离问题
为什么不能直接把模板函数定义放到 .cpp 文件?
改造方案:模板函数的显式实例化
显式实例化和全特化
模板的二阶段编译
什么是模板的二阶段编译?
第一阶段:模板定义阶段(模板声明时)
第二阶段:模板实例化阶段(使用模板时)
这有什么用?
关键词解释
分析二阶段编译和 SFINAE
与二阶段编译的关系
依赖名字(dependent name)
什么是dependent name?
为什么需要特别处理依赖名?
typename 和 template 使用对照表
名称查找
名称查找的核心阶段
1. 非依赖名称查找(non-dependent name lookup)立即查找
2.依赖名称查找(dependent name lookup)延迟查找
函数重载相关 —— 两阶段查找 + ADL
Argument-Dependent Lookup (ADL):参数相关查找
折叠表达式
什么是折叠表达式?
折叠表达式的四种形式:
概念与约束
概念(Concept)是什么?
约束(Constraint)是什么?
requires 子句
约束
合取
析取
requires 表达式
1.表达式要求(Expression Requirement)
2. 类型要求(Type Requirement)
3. 嵌套要求(Nested Requirement)/ requires 子句
4 函数调用要求(Call/Invocation Requirement)
5 复合要求(Compound Requirement)?
函数模板
函数模板不是函数,只有实例化的函数模板,编译器才能生成实际的函数定义,不过在很多时候,它看起来就像普通函数一样。
示例:
下面是一个函数模板,返回两个对象中较大的那个:
template<typename T>
T max(T a,T b){return a > b ? a : b;
}
这里其实对T有几个要求,1:有>运算符,比如内置的int,double; 2:返回了一个T,即要求T是可以移动或复制的。 如果函数模板实参不满足以上要求,则匹配不到此模板。
C++17 之前,类型 T 必须是可复制或移动才能传递参数。C++17 以后,即使复制构造函数和移动构造函数都无效,因为 C++17 强制的复制消除的存在,也可以传递临时纯右值。
使用模板
template<typename T>
T max(T a, T b) {return a > b ? a : b;
}int main(){int a{ 1 };int b{ 2 };max(a, b); // 函数模板 max 被推导为 max<int>max<double>(a, b); // 传递模板类型实参,函数模板 max 为 max<double>
}
模板函数可手动指定模板形参类型,也可以让编译器推导,即模板参数推导(template argument deduction),c++11支持函数模板参数推导,但是类模板参数推导要到c++17才支持。
对于编译无法推导的场景,可手动指定,如:
max<double>(1, 1.2);
max<std::string>("luse"s, "乐");
但是 std::string 没有办法如此操作,编译器会报:
<source>:31:4: error: call of overloaded 'max(std::string, std::string)' is ambiguous
31 | max(string("luse"), string("乐"));
| ~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:23:3: note: candidate: 'T max(const T&, const T&) [with T = std::__cxx11::basic_string<char>]'
23 | T max(const T& a, const T& b) {
| ^~~
/opt/compiler-explorer/gcc-13.2.0/include/c++/13.2.0/bits/stl_algobase.h:257:5: note: candidate: 'constexpr const _Tp& std::max(const _Tp&, const _Tp&) [with _Tp = __cxx11::basic_string<char>]'
257 | max(const _Tp& __a, const _Tp& __b)
即函数有二义性,这是因为自己所编写的max函数和标准库的max函数冲突了,但是int double实例化max函数并不会。原因是string在std命名空间,标准库的max函数也在std命名空间,虽然我们没有使用 std::
,但是根据 C++ 的查找规则,(实参依赖查找)ADL,依然可以查找到。
那么我们如何解决呢?很简单,进行有限定名字查找,即使用 ::
或 std::
说明,你到底要调用 “全局作用域”的 max,还是 std 命名空间中的 max。
::max(string("luse"), std::string("乐"));
有默认实参的模板类型形参
就如同函数形参可以有默认值一样,模板形参也可以有默认值。
template<typename T = int>
void f();f(); // 默认为 f<int>
f<double>(); // 显式指明为 f<double>using namespace std::string_literals;template<typename T1,typename T2,typename RT = decltype(true ? T1{} : T2{}) >RT max(const T1& a, const T2& b) { // RT 是 std::stringreturn a > b ? a : b;
}int main(){auto ret = ::max("1", "2"s);std::cout << ret << '\n';
}
让 max 函数模板接受两个参数的时候不需要再是相同类型,那么这自然而然就会引入另一个问题了,如何确定返回类型?
typename RT = decltype(true ? T1{} : T2{})
这是一个三目运算符表达式。然后外面使用了 decltype 获取这个表达式的类型,那么问题是,为什么是 true 呢?以及为什么需要 T1{},T2{} 这种形式?
1:我们为什么要设置为 true?
其实无所谓,设置 false 也行,true 还是 false 不会影响三目表达式的类型。这涉及到了一些复杂的规则,简单的说就是三目表达式要求第二项和第三项之间能够隐式转换,然后整个表达式的类型会是 “公共”类型。
比如第二项是 int 第三项是 double,三目表达式当然会是 double。
using T = decltype(true ? 1 : 1.2);
using T2 = decltype(false ? 1 : 1.2);
2:为什么需要 T1{}
,T2{}
这种形式?
没有办法,必须构造临时对象来写成这种形式,这里其实是不求值语境,我们只是为了写出这样一种形式,让 decltype 获取表达式的类型罢了。
模板的默认实参的和函数的默认实参大部分规则相同。
decltype(true ? T1{} : T2{})
解决了。
事实上上面的写法都十分的丑陋与麻烦,我们可以使用 auto 简化这一切。
template<typename T,typename T2>
auto max(const T& a, const T2& b) -> decltype(true ? a : b){return a > b ? a : b;
}
这是 C++11 后置返回类型,它和我们之前用默认模板实参 RT
的区别只是稍微好看了一点吗?
不,它们的返回类型是不一样的,如果函数模板的形参是类型相同 true ? a : b
表达式的类型是 const T&
;如果是 max(1, 2)
调用,那么也就是 const int&
;而前面的例子只是 T
即 int
(前面都是用模板类型参数直接构造临时对象,而不是有实际对象,自然如此,比如 T{}
)。
使用 C++20 简写函数模板,我们可以直接再简化为:
decltype(auto) max(const auto& a, const auto& b) {return a > b ? a : b;
}
效果和上面使用后置返回类型的写法完全一样;C++14 引入了两个特性:
-
返回类型推导(也就是函数可以直接写 auto 或 decltype(auto) 做返回类型,而不是像 C++11 那样,只是后置返回类型。
-
decltype(auto) “如果返回类型没有使用 decltype(auto),那么推导遵循模板实参推导的规则进行”。我们上面的
max
示例如果不使用 decltype(auto),按照模板实参的推导规则,是不会有引用和 cv 限定的,就只能推导出返回T
类型。即直接写auto会丢弃CV限定符,但decltype(auto)按decltype的规则推导类型,会保留CV限定符。
非类型模板形参
既然有”类型模板形参“,自然有非类型的,顾名思义,也就是模板不接受类型,而是接受值或对象。
template<std::size_t N>
void f() { std::cout << N << '\n'; }f<100>();
非类型模板形参有众多的规则和要求,目前,你简单认为需要参数是“常量”即可。
非类型模板形参当然也可以有默认值:
template<std::size_t N = 100>
void f() { std::cout << N << '\n'; }f(); // 默认 f<100>
f<66>(); // 显式指明 f<66>
重载函数模板
函数模板与非模板函数可以重载。
这里会涉及到非常复杂的函数重载决议,即选择到底调用哪个函数。
我们用一个简单的示例展示一部分即可:
template<typename T>
void test(T) { std::puts("template"); }void test(int) { std::puts("int"); }test(1); // 匹配到test(int)
test(1.2); // 匹配到模板
test("1"); // 匹配到模板
- 通常优先选择非模板的函数。
可变参数模板
和其他语言一样,C++ 也是支持可变参数的,我们必须使用模板才能做到。
老式 C 语言的变长实参有众多弊端。
同样的,它的规则同样众多繁琐,我们不会说太多,以后会用到的,我们当前还是在入门阶段。
我们提一个简单的需求:
我需要一个函数 sum,支持 sum(1,2,3.5,x,n...) 即函数 sum 支持任意类型,任意个数的参数进行调用,你应该如何实现?
首先就要引入一个东西:形参包
本节以 C++14 标准进行讲述。
模板形参包是接受零个或更多个模板实参(非类型、类型或模板)的模板形参。函数形参包是接受零个或更多个函数实参的函数形参。
template<typename...Args>
void sum(Args...args){}
这样一个函数,就可以接受任意类型的任意个数的参数调用,我们先观察一下它的语法和普通函数有什么不同。
模板中需要 typename 后跟三个点 Args,函数形参中需要用模板类型形参包后跟着三个点 再 args。
args 是函数形参包,Args 是类型形参包,它们的名字我们可以自定义。
args 里,就存储了我们传入的全部的参数,Args 中存储了我们传入的全部参数的类型。
那么问题来了,存储很简单,我们要如何把这些东西取出来使用呢?这就涉及到另一个知识:形参包展开。
void f(const char*, int, double) { puts("值"); }
void f(const char**, int*, double*) { puts("&"); }template<typename...Args>
void sum(Args...args){ // const char * args0, int args1, double args2f(args...); // 相当于 f(args0, args1, args2)f(&args...); // 相当于 f(&args0, &args1, &args2)
}int main() {sum("luse", 1, 1.2);
}
sum 的 Args...args
被展开为 const char * args0, int args1, double args2
。
这里我们需要定义一个术语:模式。
后随省略号且其中至少有一个形参包的名字的模式会被展开 成零个或更多个逗号分隔的模式实例。
&args...
中 &args
就是模式,在展开的时候,模式,也就是省略号前面的一整个表达式,会被不停的填入对象并添加 &
,然后逗号分隔。直至形参包的元素被消耗完。
那么根据这个,我们就能写出一些有意思的东西,比如一次性把它们打印出来:
template<typename...Args>
void print(const Args&...args){ // const char (&args0)[5], const int & args1, const double & args2int _[]{ (std::cout << args << ' ' ,0)... };
}int main() {print("luse", 1, 1.2);
}
一步一步看:(std::cout << args << ' ' ,0)...
是一个包展开,那么它的模式是:(std::cout << args << ' ' ,0)
,实际展开的时候是:
(std::cout << arg0 << ' ' ,0), (std::cout << arg1 << ' ' ,0),(std::cout << arg2 << ' ' ,0)
很明显是为了打印,对,但是为啥要括号里加个逗号零呢?这是因为逗号表达式是从左往右执行的,返回最右边的值作为整个逗号表达式的值,也就是说:每一个 (std::cout << arg0 << ' ' ,0)
都会返回 0,这主要是为了符合语法,用来初始化数组。我们创建了一个数组 int _[]
,最终这些 0 会用来初始化这个数组,当然,这个数组本身没有用,只是为了创造合适的包展开场所。
这里是以C++14来介绍本节,到了c++17引入了折叠表达式后,这里的包展开变得简单了,所以这里不用太纠结包展开场所。
我们再给出一个数组的示例:
template<typename...Args>
void print(const Args&...args) {int _[]{ (std::cout << args << ' ' ,0)... };
}template<typename T,std::size_t N, typename...Args>
void f(const T(&array)[N], Args...index) {print(array[index]...);
}int main() {int array[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };f(array, 1, 3, 5);
}
我们复用了之前写的 print 函数,我们看新的 f 函数即可。
const T(&array)[N]
注意,这是一个数组引用,我们也使用到了非类型模板形参 N
;加括号,(&array)
只是为了区分优先级。那么这里的 T
是 int,N
是 10,组成了一个数组类型。
不必感到奇怪,内建的数组类型,其 size 也是类型的一部分,这就如同 int[1]
和 int[2]
不是一个类型一样,很正常。
print(array[index]...);
其中 array[index]...
是包展开,array[index]
是模式,实际展开的时候就是:
array[arg0], array[arg1], array[arg2]
到此,如果你自己写了,理解了这两个示例,那么你应该就能正确的使用形参包展开,那就可以正确的使用基础的可变参数函数。
那么回到最初的需求,实现一个 sum
:
#include <iostream>
#include <type_traits>template<typename...Args,typename RT = std::common_type_t<Args...>>
RT sum(const Args&...args) {RT _[]{ static_cast<RT>(args)... };RT n{};for (int i = 0; i < sizeof...(args); ++i) {n += _[i];}return n;
}int main() {double ret = sum(1, 2, 3, 4, 5, 6.7);std::cout << ret << '\n'; // 21.7
}
std::common_type_t 的作用很简单,就是确定我们传入的共用类型,说白了就是这些东西都能隐式转换到哪个,那就会返回那个类型。
RT _[]{ static_cast<RT>(args)... };
创建一个数组,形参包在它的初始化器中展开,初始化这个数组,数组存储了我们传入的全部的参数。
因为窄化转换禁止了列表初始化中 int 到 double 的隐式转换,所以我们需要显式的转换为“公共类型” RT
。
其实也可以不写这么复杂,我们不用手动写循环,直接调用标准库的求和函数。
我们简化一下:
template<typename...Args,typename RT = std::common_type_t<Args...>>
RT sum(const Args&...args) {RT _[]{ args... };return std::accumulate(std::begin(_), std::end(_), RT{});
}
当然了,非类型模板形参也可以使用形参包,我们举个例子:
template<std::size_t... N>
void f(){std::size_t _[]{ N... }; // 展开相当于 1UL, 2UL, 3UL, 4UL, 5ULstd::for_each(std::begin(_), std::end(_), [](std::size_t n){std::cout << n << ' ';});
}
f<1, 2, 3, 4, 5>();
类模板
类模板不是类,只有实例化类模板,编译器才能生成实际的类。
定义类模板
下面是一个类模板,它和普通类的区别只是多了一个 template<typename T>
template<typename T>
struct Test{};
下面展示了如何使用类模板 Test
template<typename T>
struct Test {};int main(){Test<void> t;Test<int> t2;//Test t; // Error!
}
更多的例子:
template<typename T>
struct Test{T t;
};// 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>
。
类模板参数推导
对于简单的类模板,通常可以普通的类似函数模板一样的自动推导,比如前面提到的 Test
类型,又或者下面:
template<class T>
struct A{A(T, T);
};
auto y = new A{1, 2}; // 分配的类型是 A<int>
用户定义的推导指引
举个例子,我要让一个类模板,如果推导为 int,就让它实际成为 size_t:
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>
推导指引的语法还是简单的,如果只是涉及具体类型,那么只需要:
模板名称(类型a)->模板名称<想要让类型a被推导为的类型>
我们提一个稍微有点难度的需求:
template<class Ty, std::size_t size>
struct array {Ty arr[size];
};::array arr{1, 2, 3, 4, 5}; // Error!
类模板 array 同时使用了类型模板形参与非类型模板形参,保有了一个成员是数组。
它无法被我们直接推导出类型,此时就需要我们自己定义推导指引。
这会用到我们之前在函数模板里学习到的形参包。
template<typename T, typename ...Args>
array(T t,Args...) -> array<T, sizeof...(Args) + 1>;
原理很简单,我们要给出 array 的模板类型,那么就让模板形参单独写一个 T 占位,放到形参列表中,并且写一个模板类型形参包用来处理任意个参数;获取 array 的 size 也很简单,直接使用 sizeof... 获取形参包的元素个数,然后再 +1 ,因为先前我们用了一个模板形参占位。
标准库的 std::array 的推导指引,原理和这个一样。
有默认实参的模板形参
和函数模板一样,类模板一样可以有默认实参。
template<typename T = int>
struct X{};X x; // x 是 X<int> C++17 起 OK
X<> x2; // x2 是 X<int>
必须达到 C++17 有 CTAD,才可以在全局、函数作用域声明为 X
这种形式,才能省略 <>
。
标准库中也经常使用默认实参:
template<class T,class Allocator = std::allocator<T>
> class vector;template<class CharT,class Traits = std::char_traits<CharT>,class Allocator = std::allocator<CharT>
> class basic_string;
当然了,也可以给非类型模板形参以默认值,虽然不是很常见:
template<class T, std::size_t N = 10>
struct Arr
{T arr[N];
};Arr<int> x; // x 是 Arr<int,10> 它保有一个成员 int arr[10]
非类型模板形参
如上述的 struct Arr,拥有非类型模板形参,如同函数模板一样的用法。
模板模板形参
类模板的模板类型形参可以接受一个类模板作为参数,我们将它称为:模板模板形参。
先随便给出一个简单的示例:
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
是语法要求,需要声明这个模板模板形参的名字,可以自定义,这样就引入了一个模板模板形参。
介绍下为什么要有模板模板参数
先看模板模板参数:
template <typename T, template<typename, typename> class Container>
class MyWrapper {Container<T, std::allocator<T>> data;
};
这里的 Container
是一个 模板,它还需要用类型(如 T
和 std::allocator<T>
)去实例化。
比如 std::vector<int, std::allocator<int>>
就是用 int
和 std::allocator<int>
实例化 template<typename, typename>
的模板 std::vector
。
如果你写成这样
template <typename T, typename Container>
class MyWrapper {Container data; // ❌ 这时 Container 只能是一个已实例化好的类型
};
那你不能传进去 std::vector
或 std::list
这种“还没被实例化”的模板,你只能传一个具体的容器类型,比如:
MyWrapper<int, std::vector<int>> wrapper; // ✅
而你无法写:
MyWrapper<int, std::vector>; // 错,因为 std::vector 还没有指定模板参数
所以:
如果你希望传入“模板本身”,就必须用模板模板参数。
如果你只需要一个具体类型的容器,就用普通模板参数。
看更多的例子
1: 可以有名字的模板模板形参。
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 有默认模板且可以有名字的模板模板形参。
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:可以有名字的模板模板形参包。
template<typename T>
struct X{};template<typename T>
struct X2 {};template<template<typename T>typename...Ts>
struct Test{};Test<X, X2, X, X>t; // 我们可以传递任意个数的模板实参
template<std::size_t N>
struct X {};template<template<std::size_t> typename C>
struct Test {};Test<X>arr;
成员函数模板
成员函数模板基本上和普通函数模板没多大区别,唯一需要注意的是,它大致有两类:
- 类模板中的成员函数模板
- 普通类中的成员函数模板
需要注意的是:
template<typename T>
struct Class_template{void f(T) {}
};
Class_template
的成员函数 f,它不是函数模板,它就是普通的成员函数,在类模板实例化为具体类型的时候,成员函数也被实例化为具体。
1 类模板中的成员函数模板
template<typename T>
struct Class_template{template<typename... Args>void f(Args&&...args) {}
};
f
就是成员函数模板,通常写起来和普通函数模板没多大区别,大部分也都 支持,比如形参包。
2:普通类中的成员函数模板
struct Test{template<typename...Args>void f(Args&&...args){}
};
f
就是成员函数模板
可变参数类模板
形参包与包展开等知识,在类模板中是通用的。
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
类静态数据成员
struct X{static int n;
};
n 是一个 X 类的静态数据成员,它在 X 中声明,但是却没有定义,我们需要类外定义
int X::n;
或者在 C++17 以 inline 或者 constexpr 修饰。
因为 C++17 规定了 inline 修饰静态数据成员,那么这就是在类内定义,不再需要类外定义。constexpr 在 C++17 修饰静态数据成员的时候,蕴含了 inline。
struct X {inline static int n;
};struct X {constexpr static int n = 1; // constexpr 必须初始化,并且它还有 const 属性
};
与其他静态成员一样,静态数据成员模板的需要一个定义。
struct limits{template<typename T>static const T min; // 静态数据成员模板的声明
};template<typename T>
const T limits::min = {}; // 静态数据成员模板的定义
当然,如果支持 C++17 你也可以选择直接以 inline
修
变量模板
在 C++ 中,变量模板(variable template)是 C++14 引入的一种特性,允许我们为不同的类型定义一个模板变量,就像函数模板或类模板一样。它适合用于定义某些类型相关的常量。
语法形式
template<typename T>
constexpr T pi = T(3.1415926535897932385);double x = pi<double>; // pi<double> 是 double 类型
float y = pi<float>; // pi<float> 是 float 类型
有默认实参的模板形参
变量模板和函数模板、类模板一样,支持模板形参有默认实参。
template<typename T = int>
constexpr T v{};int b = v<>; // v 就是 v<int> 也就是 const int v = 0
与函数模板和类模板不同,即使模板形参有默认实参,依然要求写明 <>
。
非类型模板形参
变量模板和函数模板、类模板一样,支持非类型模板形参。
template<std::size_t N = 66>
constexpr int v = N;std::cout << v<10> << '\n';
std::cout << v<> << '\n';
模板全特化
函数模板全特化
C++函数模板全特化(Function Template Full Specialization)是指为某个具体类型或类型组合,提供该模板函数的专门版本,替代默认的通用模板实现。
// 通用模板
template<typename T>
void func(T val) {std::cout << "General template: " << val << std::endl;
}// 针对 int 类型的全特化
template<>
void func<int>(int val) {std::cout << "Specialized for int: " << val << std::endl;
}
-
template<>
表明这是一个特化版本,不再是泛型模板。 -
func<int>
指定这是对func
函数模板的int
类型特化。 -
只能对 已定义的函数模板 进行特化。
-
编译器会在调用时根据参数类型选择通用模板或特化版本。
类模板全特化
类模板全特化是指:为类模板的某个具体参数组合编写一个完整的替代实现,与原始模板完全不同、独立。全特化版本可以有完全不同的成员函数、数据成员等。
本质上:
模板全特化 ≈ 写了一个“独立的类”,只是在语义上叫它是模板的一种“专属版本”。我们要明白,写一个类的全特化,就相当于写一个新的类一样,你可以自己定义任何东西,不管是函数、数据成员、静态数据成员,等等;根据自己的需求。
示例:
// 通用模板
template<typename T>
class MyClass {
public:void show() {std::cout << "Generic version\n";}int a;int b;
};// 全特化:T = int
template<>
class MyClass<int> {
public:void show() {std::cout << "Specialized version for int\n";}string str1;string str2;
};
变量模板全特化
示例:
template<typename T>
constexpr int default_buffer_size = 4096;template<>
constexpr int default_buffer_size<char> = 1024;template<>
constexpr int default_buffer_size<wchar_t> = 2048;int size = default_buffer_size<char>; // 1024
int size2 = default_buffer_size<double>; // 4096
实现细节
以下的内容对于函数、类、变量模板都是适用的。
1. 特化必须出现在原始模板之后
template<typename T>
class A; // 声明或定义原始模板template<>
class A<int> { ... }; // 才能特化
template<typename T> // 主模板
void f(const T&){}void f2(){f(1); // 使用模板 f() 隐式实例化 f<int>
}template<> // 错误 f<int> 的显式特化在隐式实例化之后出现
void f<int>(const int&){}
在f2中实例化了f<int>, 只有又显示示例化了f<int>, 编译器会报错:
source>:35:23: error: specialization of 'void f(const T&) [with T = int]' after instantiation35 | void f<int>(const int&){}|
函数模板和变量模板的显式特化是否为 inline/constexpr/constinit/consteval 只与显式特化自身有关,主模板的声明是否带有对应说明符对它没有影响。模板声明中出现的属性在它的显式特化中也没有效果:
template<typename T>
int f(T) { return 6; }
template<>
constexpr int f<int>(int) { return 6; } // OK,f<int> 是以 constexpr 修饰的template<class T>
constexpr T g(T) { return 6; } // 这里声明的 constexpr 修饰函数模板是无效的
template<>
int g<int>(int) { return 6; } //OK,g<int> 不是以 constexpr 修饰的int main(){constexpr auto n = f<int>(0); // OK,f<int> 是以 constexpr 修饰的,可以编译期求值//constexpr auto n2 = f<double>(0); // Error! f<double> 不可编译期求值//constexpr auto n3 = g<int>(0); // Error! 函数模板 g<int> 不可编译期求值constexpr auto n4 = g<double>(0); // OK! 函数模板 g<double> 可编译期求值
}
模板类内部的特化:
struct X{template<typename T>void f(T){}template<> // 类内特化void f<int>(int){std::puts("int");}
};template<> // 类外特化
void X::f<double>(double){std::puts("void");
}X x;
x.f(1); // int
x.f(1.2); // double
x.f("");
模板偏特化
函数模板
函数模板没有“偏特化”,只有类模板 变量模板支持偏特化。
不过我们可以通过函数重载来模拟“函数模板偏特化”的行为。
// ❌ 错误:C++ 不允许函数模板偏特化
template<typename T>
void func(T val);template<typename T>
void func<T*>(T* val) { } // 编译错误!
利用重载达到类似偏特化的效果:
#include <iostream>template<typename T>
void func(T val) {std::cout << "General function template\n";
}// 模拟偏特化:指针类型的重载
template<typename T>
void func(T* val) {std::cout << "Function for pointer types\n";
}
重载是完全写了一个新的函数模板,只是同名,而偏特化语法是有<T*>修饰的。
为什么函数模板不支持偏特化呢?
Bjarne Stroustrup 和 C++ 标准委员会认为:
“函数模板的特化行为应该通过函数重载完成,而非通过偏特化。因为函数的匹配逻辑已经很复杂,再加入偏特化会让解析过程更不可控。”
简言之:
-
类模板靠“匹配度”选版本 → 特化和偏特化合理。
-
函数模板靠“重载”选版本 → 特化可以,偏特化极易二义冲突。
总结一句话:
C++ 不允许函数模板偏特化,是为了避免重载解析时出现歧义和模糊匹配问题,并且语义也和重载相冲突。函数模板的正确方式是用重载代替偏特化,用全特化处理特定类型。
类模板偏特化
偏特化(Partial Specialization) 是为类模板定义一个“部分特定”的版本,只对某些特定的模板参数形式做定制,而不是所有参数都完全固定(那叫全特化)。
例子:
template<typename T1, typename T2>
class Traits {
public:static void print() { std::cout << "General\n"; }
};// 偏特化:两个参数相同
template<typename T>
class Traits<T, T> {
public:static void print() { std::cout << "Same type\n"; }
};// 偏特化:第二个参数是 int
template<typename T>
class Traits<T, int> {
public:static void print() { std::cout << "Second is int\n"; }
};// 使用
Traits<double, double>::print(); // Same type
Traits<char, int>::print(); // Second is int
Traits<float, double>::print(); // General
变量模板偏特化
template<typename T,typename T2>
const char* s = "?";template<typename T2>
const char* s<int, T2> = "T == int";std::cout << s<char, double> << '\n'; // ?
std::cout << s<int, double> << '\n'; // T == int
std::cout << s<int, std::string> << '\n'; // T == int
SFINAE
SFINAE 是 Substitution Failure Is Not An Error(替换失败不是错误)的缩写,它是 C++ 中的一种 模板特性,允许根据模板类型的特征选择不同的函数或类模板,而不需要手动书写复杂的模板重载代码。
SFINAE 规则:
在模板实例化过程中,如果替换过程中出现错误(即类型不匹配等),编译器不会报错,而是选择其他候选的模板或者函数版本。
核心思想:
-
当模板参数替换失败时(不满足某些条件),编译器将忽略该版本模板,而不是报错。
-
这样就可以根据类型特征(如类型是否是整型、是否是指针、是否有某个成员函数等)来条件性地选择不同的函数或类模板。
示例:
#include <iostream>template<typename A>
struct B { using type = typename A::type; }; // 依赖名,C++20 之前必须使用 typename 消除歧义template<class T,class U = typename T::type, // 如果 T 没有成员 type 那么就是 SFINAE 失败(代换失败)class V = typename B<T>::type> // 如果 T 没有成员 type 那么就是硬错误 不过标准保证这里不会发生硬错误,因为到 U 的默认模板实参中的代换会首先失败
void foo(int) { std::puts("SFINAE T::type B<T>::type"); }template<typename T>
void foo(double) { std::puts("SFINAE T"); }int main(){struct C { using type = int; };foo<B<C>>(1); // void foo(int) 输出: SFINAE T::type B<T>::typefoo<void>(1); // void foo(double) 输出: SFINAE T
}
foo<B<C>>(1)
、foo<void>(1)
如果根据一般直觉,它们都会选择到 void foo(int)
,然而实际却不是如此;
这是因为 foo<void>(1);
去尝试匹配 void foo(int)
的时候,模板实参类型 void
进行替换,就会变成:
template<class void,class U = typename void::type, // SFINAE 失败class V = typename B<void>::type> // 不会发生硬错误,因为 U 的代换已经失败
void::type
这一看就是非良构,根据前面提到的:
替换的实参写出时非良构(并带有必要的诊断)的任何场合,都是替换失败。
所以这是一个替换失败,但是因为“替换失败不是错误”,只是从“重载集中丢弃这个特化,而不会导致编译失败”,然后就就去尝试匹配 void foo(double)
了,1
是 int 类型,隐式转换到 double
基础使用示例
我需要写一个函数模板 add
,想要要求传入的对象必须是支持 operator+
的,应该怎么写?
利用 SFINAE 我们能轻松完成这个需求。
template<typename T>
auto add(const T& t1, const T& t2) -> decltype(t1 + t2){ // C++11 后置返回类型,在返回类型中运用 SFINAEstd::puts("SFINAE +");return t1 + t2;
}
你可能会想这么写就行了:
template<typename T>
auto add(const T& t1, const T& t2) std::puts("SFINAE +");return t1 + t2;
}
确实,在C++14引入auto做返回值类型推导后,确实可以不用写后置返回类型推导。
但是后置返回类型推导本身也是模板实例化的一部分, 假设有一个struct X{}, 那么X类型是不支持operator + 的,那么这两种类型的写法会有什么报错呢:
第一种写法:
<source>:14:8: error: no matching function for call to 'add(X&, X&)'14 | add(x, x)| ~~~^~~~~~
<source>:4:6: note: candidate: 'template<class T> decltype ((t1 + t2)) add(const T&, const T&)'4 | auto add(const T& t1, const T& t2) -> decltype(t1 + t2) { // 后置返回类型| ^~~
<source>:4:6: note: template argument deduction/substitution failed:
<source>: In substitution of 'template<class T> decltype ((t1 + t2)) add(const T&, const T&) [with T = X]':
<source>:14:8: required from here
有类型推导的写法发生了SFINAE型的报错,即模板替换失败,而且没有找到可用的add函数了,这里的编译器报错发生在SFINAE阶段
第二种写法:
<source>: In instantiation of 'auto add(const T&, const T&) [with T = X]':
<source>:14:8: required from here
<source>:7:15: error: no match for 'operator+' (operand types are 'const X' and 'const X')7 | return t1 + t2;| ~~~^~~~
这里的报错发生在模板替换后,模板函数内部发现X类型不支持operator。第二种类型的报错是一种硬错误,即模板替换成功了,但是带入模板实参的编译失败了。
所以使用SFINAE就是在判断各个模板函数(如特化、重载)哪个更加匹配,通过模板参数替换错误来过滤掉不合适的函数,如果都不合适,那就没有找到候选函数;如果找到了,但是模板参数在函数内部使用有错,就是一个硬错误。
只有在模板的参数列表即template语句中产生类型推导的才会有SFINAE语境,第一种的decltype就有SFINAE语境。
这里的重点是什么?是模板实例化,能不要实例化就不要实例化,我们当前的示例只是因为 add
函数模板非常的简单,即使实例化错误,编译器依然可以很轻松的报错告诉你,是因为没有 operator+
。但是很多模板是非常复杂的,编译器实例化模板经常会产生一些完全不可读的报错;如果我们使用 SFINAE,编译器就是直接告诉我:“未找到匹配的重载函数”,我们自然知道就是传入的参数没有满足要求。而且实例化模板也是有开销的,很多时候甚至很大。
总而言之: 即使不为了处理重载,使用 SFINAE 约束函数模板的传入类型,也是有很大好处的:报错、编译速度。
标准库支持
std::enable_if
template<bool B, class T = void>
struct enable_if {};template<class T> // 类模板偏特化
struct enable_if<true, T> { typedef T type; }; // 只有 B 为 true,才有 type,即 ::type 才合法template< bool B, class T = void >
using enable_if_t = typename enable_if<B,T>::type; // C++14 引入
这是一个模板类,在 C++11 引入,它的用法很简单,就是第一个模板参数为 true,此模板类就有 type
,不然就没有,以此进行 SFINAE。
template<typename T,typename SFINAE = std::enable_if_t<std::is_same_v<T,int>>>
void f(T){}
函数 f
要求 T
类型必须是 int
类型;我们一步一步分析
std::enable_if_t<std::is_same_v<T,int>>>
如果 T 不是 int,那么 std::is_same_v 就会返回一个 false,也就是说 std::enable_if_t<false>
,带入:
using enable_if_t = typename enable_if<false,void>::type; // void 是默认模板实参
但是问题在于:
- enable_if 如果第一个模板参数为
false
,它根本没有type
成员。
所以这里是个代换失败,但是因为“代换失败不是错误”,所以只是不选择函数模板 f
,而不会导致编译错误。
再谈,std::enable_if
的默认模板实参是 void
,如果我们不在乎 std::enable_if
得到的类型,就让它默认就行,比如我们先前的示例 f
根本不在乎第二个模板形参 SFINAE
是啥类型。
std::void_t
template< class... >
using void_t = void;
它的实现非常非常的简单,就是一个别名,接受任意个数的类型参数,但自身始终是 void
类型。
-
将任意类型的序列映射到类型 void 的工具元函数。
-
模板元编程中,用此元函数检测 SFINAE 语境中的非良构类型
我要写一个函数模板 add
,我要求传入的对象需要支持 +
以及它需要有别名 type
,成员 value
、f
。
#include <iostream>
#include <type_traits>template<typename T,typename SFINAE = std::void_t<decltype(T{} + T{}), typename T::type, decltype(&T::value), decltype(&T::f) >>
auto add(const T& t1, const T& t2) {std::puts("SFINAE + | typename T::type | T::value");return t1 + t2;
}struct Test {int operator+(const Test& t)const {return this->value + t.value;}void f()const{}using type = void;int value;
};int main() {Test t{ 1 }, t2{ 2 };add(t, t2); // OK//add(1, 2); // 未找到匹配的重载函数
}
-
decltype(T{} + T{})
用 decltype 套起来只是为了获得类型符合语法罢了,std::void_t 只接受类型参数。如果类型没有operator+
,自然是代换失败。 -
typename T::type
使用typename
是因为依赖名称;type 本身是类型,不需要 decltype。如果add
推导的类型没有type
别名,自然是代换失败。 -
decltype(&T::value)
用 decltype 套就不用说了,&T::value
是成员指针的语法,不区分是数据成员还是成员函数,如果有这个成员value
,&类名::成员名字
自然合法,要是没有,就是代换失败。 -
decltype(&T::f)
,其实前面已经说了,成员函数是没区别的,没有成员f
就是 代换失败。
总而言之,这是为了使用 SFINAE。
那么这里
std::void_t
的作用是?
其实倒也没啥,无非就是给了个好的语境,让我们能这样写,最终 typename SFINAE = std::void_t
这里的 SFINAE
的类型就是 void
;当然了,这不重要,重要的是创造这样写的语境,能够方便我们进行 SFINAE
。
std::declval
template<class T>
typename std::add_rvalue_reference<T>::type declval() noexcept;
将任意类型 T 转换成引用类型,使得在 decltype 说明符的操作数中不必经过构造函数就能使用成员函数。
-
std::declval 只能用于 不求值语境,且不要求有定义。
-
它不能被实际调用,因此不会返回值,返回类型是
T&&
。
它常用于模板元编程 SFINAE,我们用一个示例展现它的必要性:
template<typename T, typename SFINAE = std::void_t<decltype(T{} + T{})> >
auto add(const T& t1, const T& t2) {std::puts("SFINAE +");return t1 + t2;
}struct X{int operator+(const X&)const{return 0;}
};struct X2 {X2(int){} // 有参构造,没有默认构造函数int operator+(const X2&)const {return 0;}
};int main(){X x1, x2;add(x1, x2); // OKX2 x3{ 0 }, x4{ 0 };add(x3,x4); // 未找到匹配的重载函数
}
错误的原因很简单,decltype(T{} + T{})
这个表达式中,同时要求了 T
类型支持默认构造(虽然这不是我们的本意),然而我们的 X2
类型没有默认构造,自然而然 T{}
不是合法表达式,代换失败。其实我们之前也有类似的写法,我们在本节进行纠正,使用 std::declval
:
template<typename T, typename SFINAE = std::void_t<decltype(std::declval<T>() + std::declval<T>())> >
auto add(const T& t1, const T& t2) {std::puts("SFINAE +");return t1 + t2;
}
Type Traits
Type traits” 是 C++ 模板元编程中的一个核心概念,它指的是一组在编译期操作和分析类型信息的机制,用来:
-
检测类型特性(如是否是整数类型、是否可以复制等)
-
转换类型(如移除 const、引用、数组维度等)
-
辅助模板条件分发(如通过
enable_if
、concepts
、if constexpr
控制模板重载)
"trait” 这个词的本意是“性状”或“特性”,在 C++ 中就表示:
对一个类型的属性(如是否是 POD 类型、是否可拷贝、是否是某类模板的特化)进行编译期判断或转换的手段。
Type Traits 的核心思想
1. 类型分析(Type Inspection)
你可以判断一个类型是否具备某种属性:
std::is_integral<int>::value // true
std::is_floating_point<char>::value // false
std::is_same<int, long>::value // false
2. 类型转换(Type Transformation)
对类型做一些变换,得到新类型:
std::remove_const<const int>::type // int
std::add_pointer<int>::type // int*
std::decay<int&>::type // int
3. 条件模板逻辑(SFINAE / Concepts)
通过 type traits 做“模板约束”,限制某些模板只适用于特定类型:
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
foo(T x) {return x + 1;
}
实现机制
大多数 type traits
本质上是模板的偏特化或内置元函数。以 is_same
为例:
template<typename T, typename U>
struct is_same {static constexpr bool value = false;
};template<typename T>
struct is_same<T, T> {static constexpr bool value = true;
};
与 Concepts、requires 的关系
在 C++20,concepts 和 requires 是 type traits 的升级和扩展:
-
用 concepts 语法替代
std::enable_if
更直观。 -
用 requires 表达式直接表达 “要求表达式有效”。
template<typename T>
concept Addable = requires(T a, T b) {a + b;
};
总结
类型功能 | 典型工具 | 功能说明 |
---|---|---|
类型判断 | is_same , is_integral , is_void | 判断是否具有某特征 |
类型变换 | remove_const , add_pointer | 修改类型,产生新类型 |
条件控制 | enable_if , requires , concept | 控制模板实例化条件 |
代码分支 | if constexpr , std::conditional | 编译期选择不同代码路径 |
模板显式实例化解决文件分离问题
在 C++ 中,模板显式实例化 是一种解决“模板分文件”问题的关键技术。这个问题常见于试图把模板函数或类的声明和定义放到 .h
/ .cpp
文件中时出现链接错误(undefined reference)。下面我们来详细解释它的原理、问题和解决方法。
问题:模板不能像普通函数那样分文件
为什么不能直接把模板函数定义放到 .cpp
文件?
模板是编译期生成代码的机制。编译器在遇到一个模板实例化时,需要看到完整的模板定义来生成对应代码。
// foo.h
template<typename T>
T add(T a, T b); // 只有声明// foo.cpp
template<typename T>
T add(T a, T b) { return a + b; } // 定义在 .cpp// main.cpp
#include "foo.h"
int main() { add(1, 2); } // 错:链接失败,找不到模板实例化
这会导致链接错误,因为编译器在编译 main.cpp
时无法生成 add<int>
的代码,也在 foo.cpp
中找不到它的实例化。
改造方案:模板函数的显式实例化
foo.h
// 1. 模板声明
#pragma once
template<typename T>
T add(T a, T b);
foo.cpp
#include "foo.h"// 2. 模板定义
template<typename T>
T add(T a, T b) {return a + b;
}// 3. 显式实例化
template int add<int>(int, int);
template double add<double>(double, double);
main.cpp
#include "foo.h"
int main() {int x = add(1, 2); // 使用 add<int>double y = add(1.1, 2.2); // 使用 add<double>
}
显式实例化和全特化
// 模板
template<typename T>
class MyClass {
public:void print() { std::cout << "General\n"; }
};// 全特化(新实现)
template<>
class MyClass<int> {
public:void print() { std::cout << "Specialized for int\n"; }
};// 显式实例化(强制生成代码)
template class MyClass<double>;
“模板全特化”,它实际有和显式实例化有类似效果,也是要求编译器在当前翻译单元生成我们需要的函数定义,都可以解决类似问题。
模板的二阶段编译
C++ 中的模板二阶段编译(Two-phase name lookup / compilation)是模板机制的重要规则之一,它解释了编译器是如何处理模板代码的名称查找和编译顺序的。
这个规则是理解 SFINAE、requires
、概念(concept)等高级模板编程特性时的关键。
什么是模板的二阶段编译?
模板代码的编译分两个阶段进行:
第一阶段:模板定义阶段(模板声明时)
在编译器看到模板定义本身时,它会做以下事情:
-
检查所有与模板参数无关的名称(non-dependent names),确保它们是合法的。
-
不会尝试去解析模板参数相关的代码。
template<typename T>
void f() {std::cout << sizeof(int); // ✅ 非依赖名字,立刻检查std::cout << sizeof(T); // ⛔ 依赖名字,先不检查undeclared_function(); // ⛔ 非依赖名字,立即报错(如果找不到)
}
第二阶段:模板实例化阶段(使用模板时)
当你真正使用模板(比如 f<int>()
)时,编译器:
-
替换模板参数(如 T → int)
-
这时候才解析所有依赖模板参数的名字(dependent names)
-
如果相关表达式非法或名字不存在,就报错或触发 SFINAE
template<typename T>
void f() {typename T::type x; // 依赖名字,不在第一阶段检查,等实例化时检查
}template<typename T>
void func() {foo(); // 非依赖名,第一阶段查找,必须在作用域中T::bar(); // 依赖名,第二阶段查找,只有在实例化时查找
}void foo() {}struct A {static void bar() {}
};int main() {func<A>(); // ✅ 成功
}
如果 foo()
没有定义,就会在第一阶段报错,即使你永远不会实例化 func<T>()
。
这有什么用?
-
可以用来做 SFINAE:因为依赖名字在第二阶段才查,编译器可以忽略掉非法模板实例。
-
requires
、concepts 就是对这套规则的语法糖。 -
理解为什么有些错误在模板定义时就报(non-dependent),有些要到使用时才报(dependent)
关键词解释
名称 | 说明 |
---|---|
非依赖名字 | 不依赖模板参数,比如普通函数、全局变量等,第一阶段查找 |
依赖名字 | 依赖模板参数,如 T::type 、sizeof(T) ,第二阶段处理 |
二阶段查找 | 编译器两次分别查找名字的行为 |
SFINAE | 依赖名字非法不会报错,而是“淘汰”当前模板重载 |
看一个结合了 SFINAE(Substitution Failure Is Not An Error) 和 模板二阶段编译 的实际例子:示例:判断某个类型是否有 foo()
方法
#include <iostream>
#include <type_traits>// 检查是否有 foo() 成员函数
template<typename T>
auto has_foo(int) -> decltype(std::declval<T>().foo(), std::true_type{}) {return {};
}// 备用重载:如果上面失败(替换失败),走这个
template<typename T>
std::false_type has_foo(...) {return {};
}struct A { void foo() {} }; // 有 foo()
struct B {}; // 没有 foo()int main() {std::cout << std::boolalpha;std::cout << "A has foo: " << decltype(has_foo<A>(0))::value << '\n';std::cout << "B has foo: " << decltype(has_foo<B>(0))::value << '\n';
}
分析二阶段编译和 SFINAE
-
对于
has_foo<A>(0)
,decltype(std::declval<A>().foo())
是合法的 → 匹配第一个模板。 -
对于
has_foo<B>(0)
,B
没有.foo()
,所以替换decltype(std::declval<B>().foo())
失败,但是编译器不报错(SFINAE生效),而是选中备用重载。
这就是SFINAE 的核心机制:替换失败不是错误,而是模板选择失败,自动尝试其他候选。
与二阶段编译的关系
-
T().foo()
是 依赖名字(dependent name),所以编译器不会在模板定义阶段报错。 -
当实例化
has_foo<B>
时,才发现B
没有foo()
,此时替换失败 → SFINAE 生效。
依赖名字(dependent name)
前面已经提过,现在详细解释
在 C++ 模板中,dependent name是指其解析依赖于模板参数的名字。也就是说,在模板定义时无法确定其含义,只有在实例化模板时才能确定它到底代表什么。
什么是dependent name?
一个名字是“待决”的,通常是因为它依赖于模板参数的类型或值。常见的有:
-
类型名依赖于模板参数:
template<typename T>
void func() {typename T::value_type x; // T::value_type 是一个“依赖名”
}
2.成员函数、变量名依赖于模板参数:
template<typename T>
void func() {T::someFunction(); // T::someFunction 是“依赖名”
}
为什么需要特别处理依赖名?
编译器在第一次看到模板代码时并不知道模板参数是什么,因此它不能立刻解析出像 T::value_type
这样的名称是否是一个类型、变量还是函数。
所以:
-
如果一个名字是类型,需要用
typename
明确告诉编译器。 -
如果一个名字是模板,需要用
template
明确告诉编译器。
访问模板类成员函数
template <typename T>
struct Wrapper {void call() {T t;t.template func<42>(); // 这里需要 template}
};struct Example {template<int N>void func() {std::cout << "Called func<" << N << ">()" << std::endl;}
};int main() {Wrapper<Example> w;w.call(); // 输出:Called func<42>()
}
typename
和 template
使用对照表
使用场景 | 是否需要 typename | 是否需要 template | 示例代码 |
---|---|---|---|
访问依赖类型名(模板参数的成员类型) | ✅ 需要 | ❌ 不需要 | typename T::value_type |
访问非依赖类型名(比如自己定义的) | ❌ 不需要 | ❌ 不需要 | int x; |
访问模板参数类型的嵌套模板成员类型(如 allocator) | ✅ 需要 | ✅ 需要 | typename T::template rebind<U>::other |
调用模板参数的成员模板函数(如 T::func<X>() ) | ❌ 不需要 | ✅ 需要 | t.template func<42>(); |
调用继承自模板基类的成员模板函数(如 this->func<X>() ) | ❌ 不需要 | ✅ 需要 | this->template func<42>(); |
调用非模板函数(哪怕是依赖名) | ❌ 不需要 | ❌ 不需要 | t.func(); |
访问成员变量(即使依赖于模板) | ❌ 不需要 | ❌ 不需要 | this->member; |
名称查找
名称查找的核心阶段
名称查找主要包括 两个阶段:
1. 非依赖名称查找(non-dependent name lookup)立即查找
-
在模板定义时就可以解析的名字,在模板定义时进行查找。
-
跟模板参数无关。
-
编译器在模板定义时就决定了它指向哪个名字。
-
如果之后定义了更匹配的名字,不会再考虑。
int foo() { return 1; }template<typename T>
void bar(T t) {foo(); // 已锁定全局的 foo
}int main() {int foo() { return 2; } // 不会影响模板 barbar(5); // 调用的是第一个 foo,返回 1
}
2.依赖名称查找(dependent name lookup)延迟查找
-
依赖于模板参数的名字,查找延迟到模板实例化时。
-
必须等到具体的模板参数类型已知后才能解析
-
编译器不会急着去找这个名字,等实例化再说。
template<typename T>
void func() {typename T::value_type x; // 依赖名字,等实例化时查找
}
函数重载相关 —— 两阶段查找 + ADL
Argument-Dependent Lookup (ADL):参数相关查找
如果你在调用一个函数,但没有提供限定作用域(比如 ns::func()
),编译器除了在当前作用域查找,还会去函数参数所属类型的命名空间里找。
例子:
namespace N {struct A {};void hello(A) { std::cout << "hello from N\n"; }
}void test() {N::A a;hello(a); // 找不到?错!通过 ADL 找到 N::hello
}
名称类型 | 查找时间 | 举例 |
---|---|---|
非依赖名称 | 模板定义时查找 | 普通函数名、变量名等 |
依赖名称 | 模板实例化时查找 | T::xxx , t.xxx() |
类型名 | 依赖类型名需要加 typename | typename T::value_type |
成员模板调用 | 依赖模板名需加 template | t.template func<42>() |
ADL | 参数类型命名空间 | hello(obj); |
折叠表达式
C++17 中引入的折叠表达式(Fold Expressions),是为了更方便地处理可变参数模板(variadic templates)中的参数展开问题。
什么是折叠表达式?
折叠表达式是用来将一个参数包 ...
与一个二元运算符一起“折叠”为一个表达式的语法糖。
在c++17之前的参数包展开:
template<typename...Args>
void print(const Args&...args){int _[]{ (std::cout << args << ' ' ,0)... };
}
print("luse", 1, 1.2); // luse 1 1.2
如上,只是想打印每个参数,必须创造一个数组,这样才用包展开的语法环境,很麻烦,在有折叠表达式后:
template<typename...Args>
void print(const Args&...args) {((std::cout << args << ' '), ...);
}
print("luse", 1, 1.2); // luse 1 1.2
我们一步一步分析:
(std::cout << args << ' ')
就是语法中指代的形参包(其实说的是含有形参包的运算符表达式)。那么 ,
逗号就是运算符,最后 ...
。然后最外层有括号 ()
符合语法。
函数模板实例化、折叠表达式展开,大概就是:
void print(const char(&args0)[5], const int& args1, const double& args2) {(std::cout << args0 << ' '), ((std::cout << args1 << ' '), (std::cout << args2 << ' '));
}
折叠表达式的四种形式:
类型 | 语法形式 | 展开示例(参数为 1, 2, 3 ) |
---|---|---|
一元左折叠 | (... op expr) | ((1 op 2) op 3) op expr |
一元右折叠 | (expr op ...) | expr op (1 op (2 op 3)) |
二元左折叠 | (... op ...) | ((1 op 2) op 3) |
二元右折叠 | (... op ...) | (1 op (2 op 3)) |
上述是一个一元右折叠。
左右折叠对比:
template<int...I>
constexpr int v_right = (I - ...); // 一元右折叠template<int...I>
constexpr int v_left = (... - I); // 一元左折叠int main(){std::cout << v_right<4, 5, 6> << '\n'; //(4-(5-6)) 5std::cout << v_left<4, 5, 6> << '\n'; //((4-5)-6) -7
}
举二元折叠的例子:
// 二元右折叠
template<int...I>
constexpr int v = (I + ... + 10); // 1 + (2 + (3 + (4 + 10)))
// 二元左折叠
template<int...I>
constexpr int v2 = (10 + ... + I); // (((10 + 1) + 2) + 3) + 4std::cout << v<1, 2, 3, 4> << '\n'; // 20
std::cout << v2<1, 2, 3, 4> << '\n'; // 20
概念与约束
概念(concept)是对模板参数的“要求”,而约束(constraint)是在使用模板时对模板参数“加条件”。
概念(Concept)是什么?
概念是对模板参数类型的语义要求的“名字”,用来约束模板参数必须满足某种行为或性质。
比如,我们可以定义一个“可以相加”的概念:
template<typename T>
concept Addable = requires(T a, T b) {a + b; // 要求 T 支持加法运算
};
这个 Addable
概念要求类型 T
必须支持 +
运算。
约束(Constraint)是什么?
约束是使用“概念”来限制模板参数的方式。可以通过几种语法形式:
1. 使用 requires
子句:
template<typename T>
requires Addable<T> // 使用前面定义的 Addable 概念
T add(T a, T b) {return a + b;
}
或者
template<Addable T>
auto add(const T& t1, const T& t2){std::puts("concept +");return t1 + t2;
}
语法上就是把原本的 typename
、class
换成了我们定义的 Addable
概念(concept),语义和作用也非常的明确
每个概念都是一个谓词,它在编译时求值,并在将之用作约束时成为模板接口的一部分。
也就是说我们其实可以这样:
std::cout << std::boolalpha << Addable<int> << '\n'; // true
std::cout << std::boolalpha << Addable<char[10]> << '\n'; // false
constexpr bool r = Addable<int>; // true
我想要约束:传入的对象 a b 必须都是整数类型,应该怎么写?
#include <concepts> // C++20 概念库标头decltype(auto) max(const std::integral auto& a, const std::integral auto& b) {return a > b ? a : b;
}max(1, 2); // OK
max('1', '2'); // OK
max(1u, 2u); // OK
max(1l, 2l); // OK
max(1.0, 2); // Error! 未满足关联约束
我们没有自己定义 概念(concept),而是使用了标准库的 std::integral,它的实现非常简单:
template< class T >
concept integral = std::is_integral_v<T>;
这也告诉各位我们一件事情:定义概念(concept) 时声明的约束表达式,只需要是编译期可得到 bool
类型的表达式即可。
requires
子句
关键词 requires 用来引入 requires 子句,它指定对各模板实参,或对函数声明的约束。
template<typename T>
concept add = requires(T t) { //这是requires表达式t + t;
};//下面都是requires子句
template<typename T>requires std::is_same_v<T, int>
void f(T){}template<typename T> requires add<T>
void f2(T) {}template<typename T>
void f3(T)requires requires(T t) { t + t; }
{}
requires
子句期待一个能够编译期产生 bool
值的表达式。
即:
template<typename T>requires true
void f(T){}template<typename T>requires false
void f(T){}
完全可行,各位其实可以直接带入,说白了 requires
子句引入的约束表,必须是可以编译期返回 bool
类型的值的表达式,我们前面的三个例子:std::is_same_v
、add
、requires 表达式
都如此。
约束
有两种类型的约束:
- 合取(conjunction)
- 析取(disjunction)
合取
两个约束的合取是通过在约束表达式中使用 && 运算符来构成的:
template<class T>
concept Integral = std::is_integral_v<T>;
template<class T>
concept SignedIntegral = Integral<T> && std::is_signed_v<T>;
template<class T>
concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;
析取
两个约束的析取,是通过在约束表达式中使用 || 运算符来构成的:
template<typename T>
concept number = std::integral<T> || std::floating_point<T>;
如果其中一个约束得到满足,那么两个约束的析取的到满足。析取从左到右短路求值(如果满足左侧约束,那么就不会尝试对右侧约束进行模板实参替换)。
requires
表达式
虽然前面聊概念(concept)的时候,用到了 requires
表达式(定义 concept 的时候),但是没有详细说明,本节我们详细展开说明。
注意,
requires
表达式 和requires
子句,没关系。
要求序列,是以下形式之一:
- 表达式要求
- 类型要求
- 复合要求
- 嵌套要求
1.表达式要求(Expression Requirement)
用于检查一个表达式是否合法
requires(T a) {a + a; // 检查 a + a 是否能合法编译
};
✅ 成功条件:表达式语法正确
🚫 失败示例:如果 a
是 自定义类型
,但没有定义 a + a
,则不满足。
2. 类型要求(Type Requirement)
用于检查一个类型是否存在(可推导出来)。
requires(T a) {typename T::value_type; // 检查 T 是否有成员类型 value_type
};
✅ 成功条件:类型名合法(必须是有效的类型)
🚫 失败示例:如果 T
没有定义 value_type
,则 concept 不满足。
3. 嵌套要求(Nested Requirement)/ requires 子句
用于要求一个布尔表达式计算为 true
。
requires(T a) {requires sizeof(a) > 4; // 必须返回 true,否则 concept 不成立
};
如果写
requires(T a) {sizeof(a) > 4; // 只要求sizeof(a)语句合法, > 4语句合法
};
则表明这个约束只检查sizeof(a) > 4表达式是不是合法的,而不去真的要求是不是大于4,如同上面的简单要求。
4 函数调用要求(Call/Invocation Requirement)
用于检查一个函数或操作是否可以被调用。
requires(T a) {a(); // 检查是否可以调用 a 作为函数
};
✅ 成功条件:表达式 a()
是合法调用
🚫 失败示例:如果 a
不是函数或没有重载 operator()
,则失败。
5 复合要求(Compound Requirement)?
复合要求允许你不仅测试表达式是否合法,还可以:
-
要求其返回类型
-
要求其不抛异常(noexcept)
template<typename T>
concept CanAdd = requires(T a, T b) {{ a + b } -> std::same_as<T>; // 要求 a + b 是合法表达式,且结果类型为 T
};
要求类型 | 示例 | 说明 |
---|---|---|
表达式要求 | a + b; | 表达式必须合法 |
类型要求 | typename T::type; | 类型必须存在 |
requires 子句 | requires sizeof(T) > 4; | 布尔表达式为 true |
函数调用要求 | a(); | 可调用 |
复合要求 | { a + b } noexcept -> T; | 要求合法、指定返回类型、无异常 |
requires
子句和 requires
表达式可以连用,组成 requires requires
的形式
还有在 requires
表达式中的嵌套要求,也会有 requires requires
的形式。
比较点 | requires (...) 约束子句 | requires { ... } 表达式块 |
---|---|---|
判断什么 | 判断布尔表达式是否为 true | 判断表达式是否语法合法 |
返回值要求 | 表达式必须返回 bool 类型(或可转为 bool) | 不要求返回值,只需表达式能编译通过 |
用于 | 通常用于模板函数或类头部 | 通常用于定义 concept,也可以直接用于模板 |
表达能力 | 强(可写复合逻辑) | 强(可检查多个表达式、还可以指定返回类型) |
推荐使用场景 | 简单条件组合、控制实例化 | 表达式合法性检查、定义 concept 更清晰 |