模板
模板作为 C++ 支持的第四种范程,最初与类相同,只是为了减少代码的编写。但如今,C++ 的模板元编程( TMP )已经成为了一种 图灵完备
的语言。模板编程与普通的函数式范程和面向对象范程相同,同时具备条件判断、循环、多态的特点。模板具有以下特点:
将程序从运行期提前到编译期
隐式接口和编译期多态
图灵完备的 TMP (Template Meta Programmer)
|
模板介绍
模板由关键字 template
和 typename
声明,与普通声明不同的是,模板声明的是类型而不是变量
。从而达到 “一次声明,多类型使用” 的目的。将不必要的工作移交给编译器处理,从而大大减少了代码工作量。
一个典型的模板声明与使用如下:
template <typename T, typename N>
void output(T a, N b){
cout<<a<<' '<<b<<endl;
}
int main() {
output<int, double>(10, 10.f);
output<int, const char*>(20, "hello, world");
return 0;
}
在面向对象中,一般我们习惯将函数的声明和实现进行分离,而在模板中,则需要将函数的声明和实现写在同一个文件中 |
什么是谓词?
谓词就是返回值为真或者假的函数。STL 容器中经常会使用到谓词,用于模板参数。[2]
模板参数默认值
与函数参数的默认值相同,你可以为模板参数指定默认类型。但不同的是,模板参数的默认值只是用来帮助编译器推导参数类型,而且对参数右边的参数不做要求,比如:
template <typename T, typename M = int, typename N>
void output(T a, N b){
cout<<a<<' '<<b<<endl;
}
第二个参数有默认值,而第三个参数无默认值,这是完全合法的。
模板参数推导
在某些情况下,编译器可以根据实参类型来自动推导参数类型,从而可以在尖括号内少写一部分参数:
若模板参数出现在实参列表,则使用实参类型
若模板参数未出现在实参列表,则使用默认值
若模板参数既没有出现在实参列表,也没有默认值,则需要显式指出
所有可推导的模板参数必须连续位于模板参数右边,否则可推导模板参数需要显式指出
函数的返回值对模板参数推导无用 |
模板的特化
模板本质上是对函数进行泛化,而在某些情况下可能需要对模板进行特化,以便函数的执行针对性更强
模板的特化有两种形式:
使用函数多态:适用于
不同类型之间
的特化使用
template<>
:适用于相同类型,不同值
的特化
例如:
// 类型特化
template<typename T>
void output(T a){
cout<<a<<endl;
}
void output(int a){
cout<<"int: "<<a<<endl;
}
// 值特化
template<int i>
void output(){
cout<<i<<endl;
}
template<>
void output<1>(){
cout<<"[1]"<<endl;
}
类型特化实质上就是函数重载 |
模板匹配
隐式接口和编译期多态[1]
与面向对象不同的是,模板和泛型编程最重要的是隐式接口和编译期多态,而不是显式接口和运行时多态。
例如:
template <typename T>
void doProcessing(T& w){
if(w.size() > 10 && w!= someNastyWidget){
T temp(w);
temp.normalize();
temp.swap();
}
}
从表面上来看,类型 T 必须拥有:
size, normalize, swap 成员函数,拷贝构造函数,可用于 someNastyWidget 的 operator> 函数,则就是所谓的
隐式接口
以不同的参数对函数进行调用将会导致编译器生成不同的函数,这就是所谓的
编译期多态
更近一步地,通过 typename
的第二种语法,我们可以拓展隐式接口的范围:[3]
template<typename IterT>
void workWithIterator(IterT iter){
typename iterator_traits<IterT>::value_type temp(*iter);
cout<<temp;
}
如上例,在 iterator_traits
中,为 iterator
定义了一下隐式接口。用于得到迭代器中储存的类型
struct iterator_traits<_Tp*>{
using value_type = remove_cv_t<_Tp>;
using difference_type = ptrdiff_t;
using pointer = _Tp*;
using reference = _Tp&;
};
在 C++ 20 中,可以通过 concepts 将模板参数需要满足的隐式接口进行抽离,而不是分散到整个函数中 |
条件判断和循环
要实现条件判断,可以通过模板的特化来做到:
首先实现相等判断:[4]
template <typename T1, typename T2>
struct IS_SAME{
enum{ result = 0 };
};
template<typename T>
struct IS_SAME<T, T>{
enum { result = 1};
};
现在,当用相同类型调用 IS_SAME
是,其 result = 1, 当用不同类型调用 IS_SAME
时,其 result = 0。
然后实现条件语句:
template<bool cond, typename Type1, typename Type2>
struct if_{
typedef Type1 return_type;
};
template<typename Type1, typename Type2>
struct if_<false, Type1, Type2>{
typedef Type2 return_type;
};
现在,当 if_
的第一个参数为 true 是,返回第二个模板参数的类型,当 if_
的第一个参数为 false 时,返回第三个模板参数的类型:
typename if_<
IS_SAME<int, char>::result,
int, char>::return_type a;
可见,模板中的条件判断实现的是类型判断和类型选择
与普通的循环不同的是,模板中的循环是通过对函数进行 递归
实现的:
template<int i>
void output() {
output<i - 1>();
cout << i << " ";
}
template<>
void output<1>(){
cout<<1<<" ";
}
int main() {
output<10>();
return 0;
}
显然,由于需要在模板特化时指定具体值,模板循环只适用于同一种类型
可变长模板参数
C++11引入了名为 参数包
的可变长模板参数,其声明方式和限制如下:
以
\'…
开头的参数为参数包参数包必须位于模板参数末尾
下面提供了展开参数包的方法
template<typename ...Bases>
class Derived: public Bases...{ // 在基类列表展开模板参数
Derived(const Bases... bases) : Bases(bases)...{ // 在基类初始化列表中展开
}
};
template<class... Types>
struct count {
static const std::size_t value = sizeof...(Types); // 使用 size...展开包
};
template<typename ...exceptions>
void func() throw(exceptions...){ // 动态异常展开 {cpp}17已废弃
}
通过折叠表达式展开参数包[7]
自 C++17,添加了折叠表达式用于更方便地展开参数包:
template<typename ...Args>
double sum(Args ...args){
return (args += ...);
}
当然,你依然需要注意精度的问题。
函数返回值类型提取
在某些情况下,使用模板可能会导致精度丢失:
template<typename T> T sum(T a){ return a; } template<typename T, typename ...Args> T sum(T a, Args ...args){ return a + sum(args...); }
现在我们通过一系列巧妙的设置,这样调用该函数:cout<<sum(1, 1.2);
。注意,这里由于第一个参数是 int ,因此该序列求和的结果为 int 类型。这就导致了小数部位精度丢失。
现在,我们可以让编译器自己提取返回值类型:[8]
template <typename T1, typename ...Args> struct higest_precision{ typedef typename higest_precision<Args...>::type T2; // 将 nullptr 转换为两种类型的指针,然后再解引用 typedef decltype(*((T1*)nullptr) + *((T2*) nullptr)) type; }; template<typename T> struct higest_precision<T>{ typedef T type; }; template<typename T> T sum(T a){ return a; } template<typename T, typename ...Args> typename higest_precision<T, Args...>::type sum(T a, Args ...args){ return a + sum(args...); } 对于没有使用参数包的模板来说,可以使用 ``auto + decltype`` 这种更加简单的方式:
template<typename T1, typename T2, typename T3>
auto sum(T1 a, T2 b, T3 c) -> decltype(a + b + c){
return a + b + c;
}
这种方式被称为 函数后置返回类型
奇异递归模板
奇异递归模板 ( CRTP (Curiously Recurring Template Pattern)
) 使得父类可以在编译器感知到子类的存在。主要用于:
-
- 静态多态
在使用接口规范子类时,大量运行时的类型转换会导致极大的开销,使用 CRTP 可以在编译期就获取子类,通过 static_cast 获取子类指针就避免了 dynamic_cast 的开销 +
template <typename T> class Bird{ public: T getBird(){ // 静态多态 return static_cast<T&>(*this); } }; class Chicken: public Bird<Chicken>{ }; template<typename T> const char* bird_name(Bird<T>& bird){ return typeid(bird.getBird()).name(); } int main(){ Chicken ch; std::cout<<bird_name(ch); return 0; }
-
颠倒继承 (Upside Down Inheritance)
,就是通过父类向子类添加功能。因为它的效果与普通继承父到子的逻辑是相反的。enable_shared_from_this 就是利用了颠倒继承来实现所需要的功能的。[9]
完美转发
假设我们有个函数 factory,其功能是将传入的参数包装成了一个 shared_ptr。一个简单的实现如下:
template<typename T, typename Arg>
std::shared_ptr<T> factory(Arg arg) {
return std::shared_ptr<T>(new T(arg));
}
但问题是 arg 会引起一次拷贝,为了性能考虑,可以将参数类型更改为 T&,但是这样就没法匹配到右值了。另一个方式是将参数类型更改为 const T&,但是这样函数内又没法对 arg 做处理了。
另一种方式是分别为左值和右值分别写一个函数。但是无疑十分麻烦。
完美转发就因此而生。完美转发的核心概念为引用折叠
引用折叠的规则可以简要概括为:
T& & -> T&
T&& & --> T&
T& && -> T&
T&& && -> T&&
值得注意的是 T&&,其完美保留了参数的实际类型,因此又被称为完美引用。完美引用可以同时匹配左值和右值。
借助完美引用和 std::forward,我们可以写出一个简单形式的函数:
template<typename T, typename Arg>
std::shared_ptr<T> factory(Arg&& arg) {
return std::shared_ptr<T>(new T(std::forward<T>(arg)));
}
调用的方式为:
auto ptr = factory<int>(10);
int a = 20;
auto ptr2 = factory<int>(a);