C++ templates精粹
这篇文章主要来源于C++ Templates一书,英文为《C++ Templates: The Complete Guide》
Contents
函数模板
初探函数模版
函数模板提供了一种函数行为,该函数行为可以使用多种不同的类型进行调用
template <typename T>
T max(const T& a, const T& b) {
return a < b ? b : a;
}
像这样的一个模板指定了一个“返回两个值中的最大者”的函数家族,这两个值是通过函数参数a和b传递给改函数模板的,此时参数的类型还未确定,就用模板参数T来确定。
一般的,模板参数就必须使用如下的语法来声明
template <comma-separated-list-of-parameters>
//template<用逗号隔开的参数列表>
在我们这个例子力,参数列表是typename T 。这个typename是一种关键字,到目前位置它是c++程序使用最广泛的模板参数,同样地可以用其他的没错,比如鉴于历史原因可能会有class来代替typename
函数模板通常不用声明为 inline,唯一例外的是特定类型的全特化,因为编译器可能忽略 inline,函数模板是否内联取决于编译器的优化策略
对于你定义的模版,并不是吧模板编译成一个可以处理任何类型的单一实体,而是对于实例化模版岑书的每种类型,都从模板中产生一个不同的实体,比如下面这个例子
最终编译结果中有两个独立的实例存在,而对于一开始定义的模板而言并没有单独的编译过程,而且这样的实例化过程是自动的。因此我们得到一些结论:模板在被使用时被调用了两次:
- 实例化之前,先检查模板代码本身,查看语法是否正确,在这里会发现错误的语法,比如说什么分号遗漏,类型错误等
- 在实例化期间,检查模板代码,查看是否所有的调用都有效
注意,实参的演绎是不允许自动类型转换的,你可以强制类型转换或者显式指定
模板参数
函数模板有两种类型的参数:
- 模板参数:位于函数模板名称的前面,在一对尖括号内部进行声明
- 调用参数:位于函数模板名称之后,在一对圆括号内部进行声明
template <typename T>
...max(T const& a ,T const& b)
同样地,函数模板也可以被重载,一般就是重载的参数数量,和普通非模板函数重载
模板实参推导
#include <cassert>
#include <string>
namespace jc {
template <typename T>
T max(const T& a, const T& b) {
return a < b ? b : a;
}
} // namespace jc
int main() {
assert(jc::max(1, 3) == 3); // T 推断为 int
assert(jc::max(1.0, 3.14) == 3.14); // T 推断为 double
std::string s1 = "down";
std::string s2 = "demo";
assert(jc::max(s1, s2) == "down"); // T 推断为 std::string
}
调用模板时,如果不显式指定模板参数类型,则编译器会根据传入的实参推断模板参数类型
字符串字面值传引用会推断为字符数组
(传值则推断为 const char*
,数组和函数会 decay 为指针)
对于推断不一致的情况,可以显式指定类型而不使用推断机制,或者强制转换实参为希望的类型使得推断结果一致
#include <cassert>
#include <string>
namespace jc {
template <typename T, typename U>
T max(const T& a, const U& b) {
return a < b ? b : a;
}
} // namespace jc
int main() {
std::string s = "demo";
assert(jc::max<std::string>("down", "demo") == "down");
assert(jc::max(std::string{"down"}, s) == "down");
}
- 也可以增加一个模板参数,这样每个实参的推断都是独立的,不会出现矛盾
#include <cassert>
namespace jc {
template <typename T, typename U>
T max(const T& a, const U& b) {
return a < b ? b : a;
}
} // namespace jc
int main() {
assert(jc::max(1, 3.14) == 3); // T 推断为 int,返回值截断为 int
assert(jc::max<double>(1, 3.14) == 3.14);
}
模板实参不能推断返回类型,必须显式指定(C++14 允许 auto 作为返回类型)
#include <cassert>
namespace jc {
template <typename RT, typename T, typename U>
RT max(const T& a, const U& b) {
return a < b ? b : a;
}
} // namespace jc
int main() {
assert(jc::max<double>(1, 3.14) == 3.14);
assert((jc::max<double, int, int>(1, 3.14) == 3));
}
type traits
对于类型进行计算的模板称为 type traits,也可以称为元函数,比如用 std::common_type 来计算不同类型中最通用的类型(我理解的话像是找最近公共父类这样子?)
#include <cassert>
#include <type_traits>
namespace jc {
template <typename T, typename U, typename RT = std::common_type_t<T, U>>
RT max(const T& a, const U& b) {
return a < b ? b : a;
}
} // namespace jc
int main() { assert(jc::max(1, 3.14) == 3.14); }
重载
1.当类型同时匹配普通函数和模板时,优先匹配普通函数
2.模板参数不同就会构成重载,如果对于给定的实参能同时匹配两个模板,重载解析会优先匹配更特殊的模板,如果同样特殊则产生二义性错误
#include <cassert>
namespace jc {
template <typename T, typename U>
int f(const T&, const U&) {
return 1;
}
template <typename RT, typename T, typename U>
int f(const T& a, const U& b) {
return 2;
}
} // namespace jc
int main() {
assert(jc::f(1, 3.14) == 1);
assert(jc::f<double>(1, 3.14) == 2);
// jc::f<int>(1, 3.14); // 二义性错误
}
注意不能返回 C-style 字符串的引用
namespace jc {
template <typename T>
const T& f(const char* s) {
return s;
}
} // namespace jc
int main() {
const char* s = "downdemo";
jc::f<const char*>(s); // 错误:返回临时对象的引用
}
这样的错误可能会在无意间引入
#include <cstring>
namespace jc {
template <typename T>
const T& max(const T& a, const T& b) {
return b < a ? a : b;
}
// 新增函数来支持 C-style 参数
const char* max(const char* a, const char* b) {
return std::strcmp(a, b) < 0 ? b : a;
}
template <typename T>
const T& max(const T& a, const T& b, const T& c) {
return max(max(a, b), c); // max("down", "de") 返回临时对象的引用
}
} // namespace jc
int main() {
const char* a = "down";
const char* b = "de";
const char* c = "mo";
jc::max<const char*>(a, b, c); // 错误:返回临时对象的引用
}
只有在函数调用前声明的重载才会被匹配,即使后续有更优先的匹配,由于不可见也会被忽略
字符串字面值传引用会推断为字符数组,为此需要为原始数组和字符串字面值提供特定处理的模板
类模板
类模板Stack的实现
与函数模板的处理方式亦一样,我们在一个头文件中声明和定义类Stack<>
#include <vector>
#include <stdexcept>
template <typename T>
class Stack{
private:
std::vector<T> elems;
public:
void push(T const&);
void pop();
T top() const;
bool empty() const {
return elems.empty();
}
};
template <typename T>
void Stack<T>::push(T const& elem){
Stack.push_back(elem);
}
template<typename T>
void Stack<T>::pop (){
if(elems.empty()){
throw std::out_of_range("Stack<>::pop(): empty stack");
}
elems.pop_back();
}
template <typename T>
T Stack<T>::top const{
if(elems.empty()){
throw std::out_of_range("Stack<>::top(): empty stack");
}
return elems.back();
}
可以看到类模板Stack<>是通过C++标准的vector来实现的,因此我们不需要自己实现内存管理、拷贝构造函数和赋值运算符。
类模板同样可以使用实参来特化,写成
template<>
class Stack<std::string>{
...
}
同样地,还有一些局部特化的办法(即为偏特化)
#include <cassert>
namespace jc {
template <typename T>
class A {
public:
int f() { return 1; }
};
template <typename T>
class A<T*> {
public:
int f() { return 2; }
int g() { return 3; }
};
} // namespace jc
int main() {
jc::A<int> a;//使用A<T>
assert(a.f() == 1);
jc::A<int*> b;//使用A<*T>
assert(b.f() == 2);
assert(b.g() == 3);
jc::A<jc::A<int>*> c;
assert(c.f() == 2);
assert(c.g() == 3);
}
namespace jc {
template <typename T, typename U>
struct A; // primary template
template <typename T>
struct A<T, T> {
static constexpr int i = 1;
};
template <typename T>
struct A<T, int> {
static constexpr int j = 2;
};
template <typename T, typename U>
struct A<T*, U*> {
static constexpr int k = 3;
};
} // namespace jc
using namespace jc;
static_assert(A<double, double>::i == 1);
static_assert(A<double, int>::j == 2);
static_assert(A<int*, double*>::k == 3);
int main() {
// A<int, int>{}; // 错误,匹配 A<T, T> 和 A<T, int>
// A<int*, int*>{}; // 错误,匹配 A<T, T> 和 A<T*, U*>
}
如果多个特化中,有一个匹配程度最高,则不会有二义性错误
namespace jc {
template <typename T, typename U>
struct A;
template <typename T>
struct A<T, T> {
static constexpr int i = 1;
};
template <typename T>
struct A<T, int> {
static constexpr int j = 2;
};
template <typename T, typename U>
struct A<T*, U*> {
static constexpr int k = 3;
};
template <typename T>
struct A<T*, T*> {
static constexpr int k = 4;
};
} // namespace jc
static_assert(jc::A<double, double>::i == 1);
static_assert(jc::A<double, int>::j == 2);
static_assert(jc::A<int*, double*>::k == 3);
static_assert(jc::A<double*, int*>::k == 3);
static_assert(jc::A<int*, int*>::k == 4);
static_assert(jc::A<double*, double*>::k == 4);
int main() {}
偏特化常用于元编程,偏特化遍历 std::tuple
缺省模板实参(模板的模板参数)
对于类模板,你可以为模板参数定义缺省值,这些值就被成为缺省模板实参,例如,在先前的类Stack<>中,你可以吧用于管理元素的容器定义为第2个模板参数,并且使用vector作为缺省值
namespace _space2
{
//指定缺省模板实参,并且,指定的模板实参还可以是之前的模板类型参数
template<typename T,typename U = std::vector<T> >
class stack
{
private:
U elems;
public:
stack()
{
cout << "_space2::stack" << endl;
}
public:
void pop(); //切记不能加const
T top() const;
bool empty() const;
void push(T const&);//切记不能加const
};
template<typename T,typename U>
bool stack<T,U>::empty() const
{
return elems.empty();
}
template<typename T, typename U>
void stack<T, U>::push(T const& i)
{
elems.push_back(i);
}
template<typename T, typename U>
T stack<T, U>::top() const
{
if (stack<T, U>::empty())
{
cout << "------" << endl;
return 0;
}
return elems.back();
}
//template<typename T, typename U = std::vector<T>>
template<typename T,typename U>
void stack<T, U>::pop()
{
if (stack<T, U>::empty())
{
cout << "##########" << endl;
return;
}
elems.pop_back();
}
/*
1:类模板含有两个模板参数,因此,每个成员函数的定义都必须具有这两个参数。
2:类模板参数缺省值还可以是之前的形参。
*/
}
非类型模板参数
非类型模板参数表示在编译期或链接期可以确定的常量值
在模板的设计中,也可以使用元素固定的数组来实现stack,有点在于无论是你亲自管理还是标准容器来管理内存,都可以避免这些内存管理开销,加入一个MAX_SIZE的参数来指定最多可包含的数量
template <typename T ,int MAX_SIZE>
就像这个样子的指定