文章目录
- 1 定义模板
- 1.1 类模板成员函数的实例化:
- 1.2 在类代码内简化模板类名的使用:
- 1.3 令模板自己的类型参数成为友元(C++11)
- 1.4 模板类型别名
- 1.4.1 typedef
- 1.4.2 为模板定义类型别名(C++11)
- 1.5 函数模板与类模板的区别
- 1.6 使用类的类型成员——`typename`
- 1.7 成员模板
- 1.8 控制实例化(C++11)
- 1.8.1 实例化定义会实例化所有成员
- 1.9 ⭐关于智能指针的删除器
- 1.9.1 [在运行时绑定删除器](https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-api-4.6/a01034_source.html) —— `shared_ptr`
- 1.9.1 在编译时绑定删除器 ——`unique_ptr`
- 2 模板实参推断
- 2.1 类型转换与模板类型参数
- 2.2 函数模板显示实参
- 2.3 尾置返回类型(C++11)与类型转换
- 2.4 进行类型转换的标准库模板类
- 2.5 引用折叠和右值引用参数
- 2.6 理解 `std:move`
- 2.7 在调用中使用`std::forward`保持类型信息
- 3 重载与模板
- 3.1 如何选择重载版本
- 3.2 处理C风格字符串指针和字符串字面常量
- 4 可变参数模板(C++11)
- 4.1 编写可变模板参数函数模板
- 4.2 包扩展
- 4.3 转发参数包 —— `emplace`
- 5 模板特例化
- 5.1 类模板特例化
- 5.2 类模板部分特例化(偏特化)
- 5.2.1 类模板的全特化和偏特化
- 5.2.2 通过类模板的偏特化实现`std::remove_reference`
- 5.2.3 函数模板不支持偏特化
- 5.3 间接的对函数模板进行偏特化
- 5.3.1 借助类模板偏特化
- ※ 5.3.2`Concepts`(C++20)
- 5.3.3 使用concept间接实现函数模板的偏特化
1 定义模板
1.1 类模板成员函数的实例化:
如果一个成员函数没有被使用,则它不会被实例化。成员函数只有在被用到时才进行实例化
1.2 在类代码内简化模板类名的使用:
在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参
1.3 令模板自己的类型参数成为友元(C++11)
template<typename T>
class Test2 {friend T;
private:int t2 = 0;
};class Test1 {
public:void func() {Test2<Test1> t;cout << t.t2 << endl; //访问Test2类的私有成员}
private:int t = 1;};int main() {Test1 t;t.func();return 0;
}
1.4 模板类型别名
1.4.1 typedef
- typedef可以为一个实例化的模板定义别名,但是不能定义一个typedef引用一个模板
typedef Blob<string> StrBlob; //ok
typedef Blob<T> TBlob //error
1.4.2 为模板定义类型别名(C++11)
-
template<typename T> using twin = pair<T, T>; twin<string> authors; //authors 的类型为pair<string, string>
-
template<typename T> using twin = pair<T, int>; twin<string> authors; //authors 的类型为pair<string, int>
1.5 函数模板与类模板的区别
- 默认参数:函数模板不允许由默认参数(C++11之后支持了函数模板的默认实参);类模板在模板参数列表中可以有默认参数
- 特化:函数模板只能全特化;类模板可以全特化、偏特化
- 调用方式:函数模板可以进行类型推导、隐式调用、显示调用;类模板只能显示调用
template<typename T> constexpr
auto func(const T& x, const T& y) -> decltype(x + y) {cout << typeid(T).name() << ": ";return x + y;
}int main() {int x1 = 10, y1 = 20;cout << func<int>(x1, y1) << endl; //显示double x2 = 2.2, y2 = 3.3;cout << func(x2, y2) << endl; //隐式
}
1.6 使用类的类型成员——typename
string::size_type* t;
上述代码由于编译器有string
的定义,从而知道size_type
是一个类型
- 在普通(非模板)代码中,编译器掌握类的定义,因此,他知道通过作用域访问的名字是类型还是
static
成员。
T::size_type * p;
//1.p是一个size_type类型的指针
//2.表达式为size_type这个静态成员与p相乘
如果T
是一个模板类型参数,编译器无法知道size_type
是一个类型还是一个static
成员,
- 默认情况下,C++语言假定通过作用域运算符(
::
)访问的名字不是类型。因此,我们需要手动声明使用一个模板类型参数的类型成员——typename
template <typename T>
typename T::value_type top(const T& c) {if (!c.empty()) return c.back();else return typename T::value_type(); //当容器为空,返回容器的value_type的默认值
}
1.7 成员模板
- 一个类可以包含本身是模板的成员函数。这种成员被称为成员模板
问:为什么成员模板不能是虚函数?
答:C++中的成员模板函数不能被声明为虚函数。这是因为模板函数只有在使用时才会被实例化,而虚函数表的大小在编译期间就需要确定。如果允许成员模板函数为虚函数,那么编译器就必须知道我们一共为该成员模板函数实例化了多少个不同的版本,以便确定类的虚函数表的大小。这是不可能的,因为模板函数可以被实例化为任意数量的版本,这取决于模板参数的类型和数量。
- 实例:定义
unique_ptr
的删除器
1.8 控制实例化(C++11)
在大系统中,在多个文件中实例化相同模板的额外开销可能非常严。在新标准中,我们可以通过显示实例化(explicit instantiation)来避免这种开销
//显式实例化的形式:
template [declaration]; //实例化定义extern template [declaration]; //实例化声明
//Application.cc//这些模板类型必须在程序其他位置进行实例化
extern template class Blob<string>;
extern template int compare(const int&, const int&);
Blob<string> sa1, sa2; //实例化会出现在其他位置//接收initializer_list的构造和拷贝构造函数在本文件中实例化
Blob<int> a1 = {0,1,2,3}
Blob<int> a2(a1);int i = compare(a1[0], a2[0]); //实例化出现在其他位置
//templateBuild.cc
//实例化文件必须为每个在其他文件中声明extern的类型和函数提供一个(非extern)的定义
template int compare(const int&, const int&);
template class Blob<string>; //实例化模板的所有成员
1.8.1 实例化定义会实例化所有成员
一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。因为当编译器遇到一个实例化定义时,他不了解程序使用哪些成员函数
1.9 ⭐关于智能指针的删除器
shared_ptr
:
unique_ptr
:
通过观察shared_ptr
和unique_ptr
的模板可以发现,shared_ptr
的删除器只需要在创建或reset
指针时传递给它一个可调用对象即可;而删除器类型是一个unique_ptr
对象的类型的一部分,用户必须在定义unique_ptr
时以显示模板实参的形式提供删除器类型。
1.9.1 在运行时绑定删除器 —— shared_ptr
shared_ptr
必须能直接访问其删除器,删除器必须保存为一个指针或一个封装了指针的类(如function)
int main() {int* p = new int(1);shared_ptr<int> sp(p, [](int* p) { delete(p); });int* p2 = new int(2);sp.reset(p2, [](int* p) { delete(p); cout << "haha" << endl; });//程序最后打印haha
}
我们可以确定shared_ptr
不是将删除器直接保存为一个成员,因为删除器的类型知道运行时才会确定
1.9.1 在编译时绑定删除器 ——unique_ptr
在unqiue_ptr
类中,删除器的类型是类类型的一部分,即unique_ptr
有两个模板参数.
由于删除器的类型是unique_ptr
类型的一部分,因此删除器成员的类型在编译时就确定了。从而删除器可以直接保存在unique_ptr
对象中
通过在编译时绑定删除器,
unique_ptr
避免了间接调用删除器的运行时开销。通过在运行时绑定删除器,
shared_ptr
使用户重载删除器更为方便
2 模板实参推断
2.1 类型转换与模板类型参数
template <typename T> T fobj(T t1, T t2) { return T(); }
template <typename T> T fref(const T& t1, const T& t2) { return T(); }
int main() {int x1 = 1;const int x2 = 2;fobj(x1, x2); //调用fobj(int, int)fref(x1, x2); //调用fref(const int&, const int&)int a[10], b[10];fobj(a, b); //调用f(int*, int*)fref(a, b); //error:没有与参数列表匹配的函数模板
}
不能用于函数模板的类型转换:
- 算数转换
- 派生类向基类的转换
- 用户定义的转换
2.2 函数模板显示实参
template<typename T1, typename T2, typename T3>
T1 sum(T2 x, T3 y) {return x + y;
}int main() {cout << sum<int>(1.1, 1) << endl; //2cout << sum<double>(1.1, 1) << endl; //2.1
}
2.3 尾置返回类型(C++11)与类型转换
template <typename It>
auto fcn(It beg, It en) -> decltype(*beg) {return *beg;
}
解引用返回一个序列的左值,因此通过decltype
推断的类型为beg
表示的元素的类型的引用
2.4 进行类型转换的标准库模板类
- 其中
type
为remove_reference
模板类的一个类型成员
template<typename It>
//为了使用模板参数的成员,type是一个类的成员,而该类依赖于一个模板参数,因此我们必须使用typename
//type的类型为非引用的
auto func2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type {reutrn *beg;
}
其他标准类型转换模板在书606页
习题:
template<typename It>
//为了使用模板参数的成员,必须用typename
auto func2(It beg, It end) -> decltype(*beg + 0) { //合法:左值引用 + 右值 -> 左值return *beg; //返回序列中一个元素的拷贝
}int main() {list<int> lt{ 1,2 };decltype(func2(lt.begin(), lt.end())) t = 10;
}
2.5 引用折叠和右值引用参数
template <typename T> void func(T&&);int main() {int i = 0;func(i); //看起来实参的类型为int& &&
}
- 我们不能直接定义一个引用的引用, 但是可以通过类型别名或模板类型参数间接定义。
- 新标准中(C++11), 折叠规则拓展到右值引用
X& &
、X& &&
、X&& &
--折叠–>X&
X&& &&
--折叠–>X&&
typedef int& ref_type;
typedef ref_type&& refs_type;
int main() {int x = 10;refs_type y = x; //y是一个左值引用:int& && -> 引用折叠 -> int&
}
2.6 理解 std:move
//std::move是如何定义的:
template <typename T>
typename remove_reference<T>::type&& move(T&& t) {return static_cast<typename remove_reference<T>::type&&>(t);
}//考虑以下两种情况 P611
int main() {string s1("lvalue s1"), s2;s = move(string("rvalue s")); //右值移动s = move(s1); //左值移动,移动之后,s1的值是不确定的
}
2.7 在调用中使用std::forward
保持类型信息
3 重载与模板
3.1 如何选择重载版本
- 当有多个重载模板对一个调用提供同样好的匹配时,因该选择最特例化的版本
template <typename T> string debug_rep(const T& t) {}template <typename T> string debug_rep(T* t) {}int main() {string s("ss");const string* sp = &s;debug_rep(sp); //调用第二个,T被绑定到const string
}
- 如果一个非模板函数和一个模板函数提供同样好的匹配,则选择非模板版本
3.2 处理C风格字符串指针和字符串字面常量
4 可变参数模板(C++11)
4.1 编写可变模板参数函数模板
template <typename T>ostream& print(ostream& os, const T& t) {return os << t;}template <typename T, typename... Args>ostream& print(ostream& os, const T& t, const Args&... args) {os << t << ' ';return print(os, args...); //包拓展}
4.2 包扩展
- 函数参数包名 +
...
print(os, args...);
- function(函数参数包名) +
...
print(os, debug_rep(args...)); //扩展中的模式会独立地应用于包中的元素
4.3 转发参数包 —— emplace
template <class... Args>
inline
void StrVec::emplace_back(Args&&... args) {chk_n_alloc();alloc.construct(first_free++, std::forward<Args>(args)...);
}
5 模板特例化
5.1 类模板特例化
namespace std {template<>struct hash<Sales_data> {//用来散列一个无需容器的类型必须要定义下列类型typedef size_t result_type;typedef Sales_data argument_type; //默认情况下,此类型需要operator==size_t operator()(const Sales_data& s) const {return hash<string>() (s.bookNo) ^ //构造匿名对象调用operator()返回一个哈希值hash<unsigned>() (s.units_sold) ^ hash<double>() (s.revenue);}};
} //关闭std命名空间
5.2 类模板部分特例化(偏特化)
5.2.1 类模板的全特化和偏特化
我们只能部分特例化类模板,而不能部分特例化函数模板 —— P628
template <typename T>
class add {
public:T operator()(T a, T b) {return a + b;}
};template <typename T> //类模板的全特化
class add<T*> {
public:T operator()(T* a, T* b) {cout << "pointer" << endl;return *a + *b;}
};template<>
class add<int*> { //类模板的偏特化
public:int operator()(int* a, int* b) {cout << "int pointer" << endl;return *a + *b;}
};
5.2.2 通过类模板的偏特化实现std::remove_reference
代码更具模板的匹配规则进行精确的匹配,当传入的是一个右值引用时,匹配第三个模板,此时T的类型为去掉右值引用性质之后的类型
template<class T> struct remove_reference {typedef T type;
};
template<class T> struct remove_reference<T&> {typedef T type;
};
template<class T> struct remove_reference<T&&> {typedef T type;
};
5.2.3 函数模板不支持偏特化
问:为什么函数模板不能支持偏特化,如果可以,发产生怎样的歧义?
template <typename T> T add(T a, T b) { return a + b; }template <typename T*> //函数模板的偏特化,错误,产生歧义 T add(T* a, T* b) { return *a + *b; }template<> int add(int a, int b) { //函数模板的全特化return a + b; }/* template<> class add<int*> { //类模板的偏特化 public:int operator()(int* a, int* b) {cout << "int pointer" << endl;return *a + *b;} }; */
在这个例子中,如果我们尝试调用add(a, b),其中a和b都是指针类型,编译器将无法确定应该调用哪个函数。因为第一个函数模板可以通过类型推导将T推导为指针类型,而第二个函数模板是针对指针类型的偏特化版本。这就产生了歧义,编译器无法确定应该调用哪个函数。
5.3 间接的对函数模板进行偏特化
C++标准并不支持函数模板偏特化。然而在实际开发中,我们确实需要对一些函数模板进行偏特化。这是因为偏特化和函数重载容易出现歧义。
-
借助类模板偏特化:由于类可以进行偏特化处理,因此一种非常直观的方案就是使用Functor代替函数,并实现 operator ()。详细例子看5.2.1
-
使用
Concepts
:C++20提供了 Concepts 特性,Concepts
特性提出的动机是为了解决模板元编程过程中,编译器给出的报错信息冗余及编译器不能很好的给出准确的出错信息的问题。
5.3.1 借助类模板偏特化
template <typename T>
class add {
public:T operator()(T a, T b) {return a + b;}
};
template <typename T> //全特化
class add<T*> {
public:T operator()(T* a, T* b) {cout << "pointer" << endl;return *a + *b;}
};
template<>
class add<int*> { //偏特化
public:int operator()(int* a, int* b) {cout << "int pointer" << endl;return *a + *b;}
};int main() {cout << add<int>()(10, 20) << endl;int x = 10, y = 20;cout << add<int*>()(&x, &y) << endl;
}
※ 5.3.2Concepts
(C++20)
Concepts的提出就是为了解决上述问题,它通过将模板的类型约束抽象出来,然后在模板定义时再使用。这样成功解耦了模板类型约束和模板本身的一些类型逻辑
concept
的定义形式
template < template-parameter-list > concept concept-name = constraint-expression;
其中,constraint-expression
是一个可以被eval
为bool
的表达式或者编译期函数。在使用定义好的concept
时,constraint-expression
会根据上面 template-parameter-list
传入的类型,执行编译期计算,判断使用该concept的模板定义是否满足。如果不满足,则编译期会给定一个具有明确语义的错误
template <typename T>
concept always_satisfied = true;/* 将类型T抽象成integral类型,当模板实参使constraint-expression为true时,才匹配template<inntegral T>的函数模板*/
template <typename T>
concept integral = std::is_integral_v<T>; template <typename T>
concept signed_integral = integral<T> && std::is_signed_v<T>;template <always_satisfied T>
T mul(T a, T b) {puts("always_satisfied");return a * b;
}template <integral T>
T add(T a, T b) {puts("integral");return a + b;
}template <signed_integral T>
T subtract(T a, T b) {puts("signed_integral");return a - b;
}int main() {mul(1, 2);mul(1.0f, 2.0f);add(1, 2);subtract(1, 2);
}
5.3.3 使用concept间接实现函数模板的偏特化
- 我们以5.2.2的代码为基准,使用concept编写函数模板的偏特化
//T是一个整数类型
template <typename T>
concept integral = std::is_integral_v<T>;//T为一个整数类型的指针,特别注意,使用remove_pointer去除模板实参的指针性质
template <typename T>
concept integral_pointer = integral<typename std::remove_pointer<T>::type> && std::is_pointer_v<T>;template <integral T>
void add(T a, T b) {cout << "integral: " << a + b << endl;
}template <integral_pointer T>
void add(T a, T b) {cout << "integral_pointer: " << *a + *b << endl;
}int main() {int x = 10, y = 20;add(&x, &y);add(10, 20);/*output:integral_pointer: 30integral: 30*/float a = 1.1, b = 2.2;add(a, b); //error:没有于参数列表匹配的重载函数,add实例参数类型为(float, float)add(&a, &b); //error:没有于参数列表匹配的重载函数,add实例参数类型为(float*, float*)
}
template
concept integral_pointer = integral<typename std::remove_pointer::type> && std::is_pointer_v;
template
void add(T a, T b) {
cout << "integral: " << a + b << endl;
}
template <integral_pointer T>
void add(T a, T b) {
cout << "integral_pointer: " << *a + *b << endl;
}
int main() {
int x = 10, y = 20;
add(&x, &y);
add(10, 20);
/*
output:
integral_pointer: 30
integral: 30
*/
float a = 1.1, b = 2.2;
add(a, b); //error:没有于参数列表匹配的重载函数,add实例参数类型为(float, float)
add(&a, &b); //error:没有于参数列表匹配的重载函数,add实例参数类型为(float*, float*)
}