第十六章 模版与泛型编程
?
OOP: 能处理类型在程序运行之前都未知的情况
泛型编程: 在编译时就能获知类型
泛型代码的两个重要原则:
- 模板中的函数参数是const的应用(使用const可以接受更多的类型, 拷贝/不可拷贝/const/非const的)
- 函数体重的条件判断仅使用<比较运算符(减少对类型的要求, <可以计算出!=, >, ==).
16.1 定义模版
16.1.1 函数模版
代表 任意类型.? 类型参数
template <typename T, typename W, typename A > // 指定一个类型为T
int compare(const T& v1, const W& v2) { // 模版中指定的类型
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
模版参数列表的作用很像函数参数列表
类型参数前必须使用class 或 typename, 在模板参数列表中, 这俩没有什么不同.
类型已经指定了, 代表任意值. 非类型参数, 要求必须是常量表达式.
template<unsigned N, unsigned M>
int compare(const char(&p1)[N], const char(&p2)[m]) {
return strcmp(p1, p2);
}
inline和constexpr 的函数模板:
inline或constexpr放在模板参数之后, 返回类型之前
template <typename T> inline T min(const T&, const T&);
函数模板和类模板成员函数的定义通常放在头文件中.
16.1.2 类模板
编译器不能为类模板推断模板参数类型. 感觉就像 需要使用string的地方全部换成T.
template<typename T>
class Blob {
public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
Blob():data(std::make_shared<std::vector<T>>()) {};
Blob(std::initializer_list<T> il) :data(std::make_shared<std::vector<T>>(il)) {};
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
void push_back(const T& t) { data->push_back(t); }
void push_back(T&& t) { data->push_back(std::move(t)); }
void pop_back();
T& back();
T& operator[](size_type i);
private:
std::shared_ptr<std::vector<T>> data;
Blob check(size_type i, const T& msg) const;
};
模板形成的各个类 之间没有任何关系.
当在类外完成内类声明的函数时, 仍然需要使用template<typename T> 声明类型T, 并且 需要用到类名的地方, 必须在类名后面增加<T>, 如 check函数的定义为:
template<typename T>
Blob<T> Blob<T>::check(size_type i, const T& msg) const { // 类模板外使用 类模板名时需要带<T>
// xx
}
使用类模板时:
int main(int argc, char** argv) {
Blob<int> ia = { 0,1,2,3,4 };
}
模板嵌套, 和在类模板作用域内时可以省略<T>, 在类模板外使用类模板名时需要BlobPtr<T>
template <typename T> class BlobPtr {
public:
BlobPtr() :curr(0) {}
BlobPtr(Blob<T> &a, size_t sz=0):wptr(a.data), curr(sz) {} // 使用别的类模板时, 需要Blob<T>
T& operator*() const {
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
BlobPtr& operator++(); // 类模板作用域内 不需要使用BlobPtr<T>& operator++()
BlobPtr& operator--(); // 等同于 BlobPtr<T>& operator--()
private:
std::shared_ptr<std::vector<T>>check(std::size_t, const std::string&) const;
std::weak_ptr<std::vector<T>> wptr;
std::size_t curr;
};
类模板和友元:
类模板包含的非模板友元, 则友元可以访问所有模板实例
类模板包含模板友元, 则所有友元实例可以访问所有模板实例, 也可以只授权特定友元.
一对一友好关系:
template <typename> class BlobPtr; // Blob内定义友元类 使用
template<typename> class Blob; // 用于operator==的函数声明
template<typename T> // 用于operator==的函数声明
bool operator==(const Blob<T>&, const Blob<T>&); // 函数声明
template<typename T>
class Blob {
public:
// typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
friend class BlobPtr<T>; //友元类
friend bool operator==<T>(const Blob<T>&, const Blob<T>&); // 友元函数声明
// xx
}
上述代码中, 友元类的声明中 BlobPtr<T> 和友元函数 在Blob中的声明中, 使用的类型都是T, 如此则限定了, BlobPtr, operator= 中使用的类型必须和Blob中的类型一致. 即: 友好关系 被限定在所有使用相同类型的Blob, BlobPtr, operator== 中.
通用和特定的模板友好关系:
分为两种, 一种是非模板类 对模板类:
template<typename T> class Pal; //类模板的前置声明
class C {
friend class Pal<C>; // 只用用C实例化的类Pal才是C的一个友元.
template <typename T> friend class Pal2; // Pal2的所有实例都是C的友元, 此种情况不需要前置声明
};
?另一种, 模板类对 其他:
template<typename T> class Pal; // 类模板的前置声明
template<typename T> class C2 {
friend class Pal<T>; // 类型为T的C2的实例和Pal的实例才是友好关系. 一对一
template <typename X> friend class Pal2; // X可能等于T,也可能不等于T, 所有就是任意类型的C2的实例和Pal2实例都是友元关系, 多对多
friend class Pal3; // Pal3 不需要前置声明, 所有类型的C2实例都与Pal3 是友元关系, 多对一.
};
上面代码中, 两种情况下都是一对一的需要前置声明, 其余的则不需要.
模板参数也可以声明为友元:
template<typename T> class B {
friend T;
};
模板类型别名:? 因为Blob<T>不是一个确切的类型, 所有不能使用typedef 重命名, 必须是确定的类型才可以重命名; 但是 可以这么命名
typedef Blob<T> StrBlob; // 错误
typedef Blob<string> StrBlob;
template<typename T> using twin = pair<T, T>; // 定义
twin<string>authors; // == pair<string, string> 使用方法
template<typename T> using twin2 = pair<int, T>; // 也可以固定一个类型
类模板的static成员: 所有的类模板实例 都共享相同的static成员
template<typename T> class Foo {
public:
static std::size_t cout() { return ctr; }
private:
static std::size_t ctr;
};
template<typename T> size_t Foo<T>::ctr = 0; // 定义 跟非static成员定义相似
int main(int argc, char** argv) {
Foo<int> fi;
auto ct = Foo<int>::cout();
ct = fi.cout();
ct = Foo::cout(); // 错误, 无法确定模板实例, 所以无法使用cout函数
}
static 成员需要在模板外定义初始值
模板外部使用模板名时需要携带类型列表
// 16.16
template <typename T>
class Vec {
public:
Vec() :elements(nullptr), first_free(nullptr), cap(nullptr) {}
Vec(const Vec&);
Vec(const std::initializer_list<T>&);
Vec& operator=(const Vec&);
Vec& operator=(Vec&&) noexcept;
Vec(Vec&&) noexcept;
~Vec();
void push_back(const T&);
void push_back(const T&&);
size_t size() const { return first_free - elements; }
size_t capacity() const { return cap - elements; }
void resize(size_t);
void resize(size_t, const T&);
// 若 new_cap 大于当前的 capacity() ,则分配新存储,否则该方法不做任何事。
void reserve(size_t); // 增加vector的容量到大于或者等于 指定的值
T* begin() const { return elements; }
T* end() const { return first_free; }
private:
static std::allocator<T> alloc; // 用于分配元素内存
// 如果没有空间容纳新元素了, 则重新分配内存
void chk_n_alloc() {
if (size() == capacity()) reallocate();
}
// 用于分配足够的内存保存给定范围的元素, 并将这些元素拷贝到新分配的内存中.返回的pair中的两个指针
// 分别指向新空间的开始位置和尾后位置.
std::pair<T*, T*>alloc_n_copy(const T*, const T*);
void free(); // 销毁元素并释放内存
void reallocate(size_t); // 获得更多内存并拷贝已有元素
T* elements; // 指向首元素的指针
T* first_free; // 指向第一个空闲元素的指针
T* cap; // 指向尾后位置的指针
};
template<typename T>
std::allocator<T> Vec<T>::alloc = std::allocator<T>();
template<typename T>
void Vec<T>::push_back(const T& s) {
chk_n_alloc();
alloc.construct(first_free++, s);
}
template<typename T>
void Vec<T>::push_back(const T&& s) {
chk_n_alloc();
alloc.construct(first_free++, std::move(s));
}
template<typename T>
std::pair<T*, T*>Vec<T>::alloc_n_copy(const T* b, const T* e) {
auto data = alloc.allocate(e - b);
return { data, uninitialized_copy(b,e, data) };
}
template<typename T>
void Vec<T>::free() {
// elements 如果为空 说明该vec为空, 则不必释放内存
if (elements) {
for_each(elements, first_free, [this](T& s) { alloc.destroy(&s); });
alloc.deallocate(elements, cap - elements);
}
}
template<typename T>
Vec<T>::Vec(const Vec<T>& s) {
auto newData = alloc_n_copy(s.begin(), s.end());
elements = newData.first;
first_free = cap = newData.second;
}
template<typename T>
Vec<T>::~Vec() {
free();
}
template<typename T>
Vec<T>& Vec<T>::operator=(const Vec<T>& rhs) {
auto data = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
template<typename T>
Vec<T>& Vec<T>::operator=(Vec<T>&& rrs) noexcept {
if (this != &rrs) {
free();
elements = rrs.elements;
first_free = rrs.first_free;
cap = rrs.cap;
rrs.elements = rrs.first_free = rrs.cap = nullptr;
}
return *this;
};
template<typename T>
void Vec<T>::reallocate(size_t new_size) {
size_t newcapacity = 0;
if (new_size == 0) newcapacity = size() ? 2 * size() : 1;
else newcapacity = new_size;
auto newdata = alloc.allocate(newcapacity);
auto dest = newdata;
auto elem = elements;
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
free();
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
template<typename T>
void Vec<T>::reserve(size_t new_cap) {
if (new_cap > capacity()) {
reallocate(new_cap);
}
}
template<typename T>
void Vec<T>::resize(size_t count) { resize(count, T()); }
template<typename T>
void Vec<T>::resize(size_t count, const T& s) {
if (count > size()) {
if (count > capacity()) reallocate(2 * count);
for (size_t t = size(); t != count; ++t) {
alloc.construct(first_free++, s);
}
}
else if (count < size()) {
while (first_free != elements + count) alloc.destroy(--first_free);
}
}
template<typename T>
Vec<T>::Vec(const std::initializer_list<T>& lst) {
auto newdata = alloc_n_copy(lst.begin(), lst.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
template<typename T>
Vec<T>::Vec(Vec<T>&& s) noexcept :elements(s.elements), first_free(s.first_free), cap(s.cap) {
s.elements = s.first_free = s.cap = nullptr;
}
16.1.3 模板参数
- 模板参数名的可用范围为 其声明之后 至 模板声明或定义结束之前
- 模板参数隐藏外层作用域中声明的相同名字
- 参数名不能被重新定义
模板声明时必须包含模板参数, 声明和定义中使用typename 后的名字可以不同
使用类的类型成员:
默认通过作用域运算符访问的名字不是类型, 是静态成员, 如vec模板中的alloc
template<typename T> std::allocator<T> Vec<T>::alloc = std::allocator<T>();
如果希望使用一个模板类型参数的类型成员, 则必须使用typename 明确
template<typename T>
typename T::value_type top(const T& c) {
if (!c.empty()) return c.back();
else return typename T::value_type();
}
当希望通知编译器一个名字表示类型时, 必须使用typename, 而不能使用class.
// 这个需要练习, 琢磨
默认模板实参:
template<typename T, typename F=less<T>()> // less<T> less<int> 比较函数
bool compare(const T& v1, const T& v2, F f) {
if (f(v1, v2)) return -1;
if (f(v2, v1)) return 1;
return 0;
}
模板默认实参与类模板:
无论何时使用一个类模板(类作用于内 可以不用) , 都必须在模板名后接上尖括号.
template<typename T = int> class Numbers { // 默认为int类型 typename 也可以用class
public:
Numbers(T v = 0) :val(v) {};
private:
T val;
};
int main(int argc, char** argv) {
Numbers<long double> lots;
Numbers<> aaa; // 默认int
}
16.1.4 成员模板
类内是模板的成员函数叫成员模板, 不能是虚函数. 成员模板 有自己的参数类型
template<typename T>
class NewCall {
template<typename TT> void funInC(TT& a) {
//xx
}
template<typename TT> void funInC2(TT&);
};
template<typename T> // 需要类 类型参数, 同时需要成员模板 类型参数
template<typename WW> // 此处的类型参数可以与 类内定义的类型参数不同
void NewCall<T>::funInC2(WW& w) {
//xx
};
实例化与成员模板: 编译器会根据传入的类 类型推断 模板类的类型, 会根据成员函数传入的参数推断 成员模板 的类型.
16.1.5 控制实例化
当模板被使用时才会进行实例化.? 显式实例化
extern template declaration; // 实例化声明
template declaration; // 实例化定义
// 如
extern template class Blob<string>; // 声明
template int compare(const int&, const int&); // 定义
当编译器遇到extern声明时, 不会在本文件中实例化代码, 并且承诺在程序其他位置有该实例化的一个非extern声明/定义. 可以有多个extern声明, 必须只有一个定义.
extern 必须出现在任何使用模板之前, 否则会自动实例化.
实例化定义 会实例化所有成员.
可能模板会被实例化的几种情况
- 声明一个类模板的指针和引用,不会引起类模板的实例化,因为没有必要知道该类的定义。
- 定义一个类类型的对象时需要该类的定义,因此类模板会被实例化。
- 在使用sizeof()时,因为需要计算对象的大小,编译器必须根据类型将其实例化出来
- new 类模板被实例化
- 引用类模板的成员会导致类模板被编译器实例化。
- 需要注意的是,类模板的成员函数本身也是一个模板。标准C++要求这样的成员函数只有在被调用或者取地址的时候,才被实例化
16.1.6 效率和灵活性
//16.28 练习题? 参考?https://github.com/pezy/CppPrimer/tree/master/ch16
16.2 模版实参推断
16.2.1 类型转换与模板类型参数
const转换:? 非const的引用/指针 可以传递给一个const的
template<typename T> T fobj(T, T);
template<typename T> T fref(const T&, const T&);
string s1("aaaaaa");
const string s2("bbbb");
fobj(s1, s2); // s2 的const被忽略
fref(s1, s2); // s1 非const转为const
数组或函数指针: 如果形参不是引用类型,? 则数组/函数类型的实参可以应用正常的指针转换, 即 数组实参转换为一个指向首元素的指针, 函数实参转换为一个指向该函数类型的指针.
template<typename T> T fobj(T, T);
template<typename T> T fref(const T&, const T&);
int a[10], b[11];
fobj(a, b); // a[10] -> int*, b[11]-> int* fobj(int*, int*)
fref(a, b); // 错误的 引用类型的形参时, 实参不转换 a[10], b[11],不是一种类型
将实参传递给带模板类型的函数形参时, 只有const转换, 数组和函数到指针的转换.
如果函数参数类型不是模板参数, 则对实参进行正常的类型转换.
// 16.34
template<typename T> int compare2(const T&, const T&);
compare2("hi", "world"); // 不合法, "hi" 转化为const char[3], "world" 转化为const char[6] 两者类型不同
compare2("bye", "daa"); // 合法, 都转化为const char(&)[4]
16.2.2 函数模板显示实参
在函数名的后面跟上<类型> 可以显式声明模板实参 如:
template<typename T>
bool compare(const T& v1, const T& v2) {
if (v1<v2) return -1;
if (v2<v1) return 1;
return 0;
}
// 使用
auto a = compare<std::string>("helo", "world");
16.2.3 尾置返回类型与类型转换
当出现 需要返回值类型, 但是在没确定参数类型时 无法确定返回值类型的 情况时, 需要使用尾置指明返回值类型:
template<typename It>
auto fcn(It beg, It end) -> decltype(*beg) {
return *beg;
}
尾置返回 允许我们在参数列表之后 声明返回类型.
上面的代码只能返回元素的引用,? 如果想返回一个元素的拷贝 则需要使用下面的代码
template<typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type {
return *beg;
}
remove_reference 为标准库函数, 其作用是 脱去引用类型的 引用部分, 其中的type 则返回脱去引用部分后剩余的类型.
如: remove_reference<int&>::type? 的值为int.
类似remove_reference<T>::type 的函数还有:
?16.2.4 函数指针和实参推断
可以通过函数指针 实例化 函数模板. 重载的函数需要明确究竟是哪个版本的模板.
template<typename T> int compare(const T&, const T&);
// 函数重载
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
// 使用
int (*pf1)(const int&, const int&) = compare; // T = int
func(compare<int>); // 调用重载的func, 但是需要指明compare到底是哪个? 是string版本还是int版本
当参数是一个? 函数模板实例的地址? 时, 程序必须满足 对每个模板参数,能唯一确定其类型或值
16.2.5 模板实参推断和引用
从左值引用函数参数推断类型:?
template<typename T> void f1(T&) 需要传入的类型 必须是一个左值. 如果传入的const int, 则T就是const int
template<typename T>void f2(const T&) 可以传入任何参数包括一个右值. 此时会忽略掉顶层const. 如f2(ci) 如果ci 是const int的, 则T 仍然是int的
template<typename T>void f3(T&&) 右值推断和左值相同
引用折叠和右值引用参数:
两种列外:
- 当我们将一个左值(如i)传递给函数的右值引用参数(如f3), 且此右值引用指向模板类型参数(如T&&)时, 编译器推断模板类型参数为实参(i)的左值引用类型. 所以f3(i) 中 T的类型为int&
- 引用会折叠. 如 X& &, X& &&, X&& & 都会折叠成类型X&,? X&& && 折叠成 X&&
引用折叠只能应用于简介创建的引用的引用, 如果类型别名或模板参数.
16.2.6 std::move
static_cast 可以显式的将一个左值转换为一个右值引用.?
template<typename T>
typename remove_reference<T>::type&& move(T&& t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}
// 当move传入的是一个左值时, 就变成string&& move(string &t)
// static_cast<string&&>(t) t的类型为string&, cast 将一个左值引用转化为右值引用
16.2.7 转发
可以利用 模板参数 是右值引用的特性, 保留 函数的参数的所有属性, 即: 如果一个函数参数是指向模板类型参数的右值引用(T&&), 它对应的实参的const属性和左值/右值属性将得到保持. 当用于一个指向模板参数类型的右值引用函数参数(T&&)时, forward 会保持实参类型的所有细节.
template<typename F, typename T1, typename T2>
void flip(F f, T1&& t1, T2&& t2) {
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
// 此种定义可以完美保存t1, t2的属性, 并实现反转
16.3 重载与模版
函数模板可以被另一个模板或普通非模板函数重载.
重载后的匹配规则:
- 对于一个调用, 其候选函数包括所有模板参数推断成功的函数模板实例
- 候选的函数模板总是可行的, 因为模板实参推断会排除任何不可行的模板.
- 可行函数(模板和非模板)按类型转换来排序
- 如果有一个函数提供比任何其他函数都更好的匹配, 则选择此函数.如果有多个函数提供同样好的匹配, 则:
- 如果同样好的函数中只有一个是非模板函数, 则选择此函数.
- 如果同样好的函数中没有非模板函数, 而有多个函数模板, 且其中一个模板比其他模板更特例化, 则选择此模板
- 否则, 此调用有歧义
16.4 可变参数模版
可以接收可变数目参数的模板函数或模板类. 可变数目的参数 -- 参数包. 符号 ...? 省略号,
typename... T
两种参数包:
- 模板参数包 -- 零或多个 模板参数
- 函数参数包 -- 零或多个函数参数
template<typename T, typename ... Args>
void foo(const T& t, const Args& ... rest) {}
sizeof... 运算符
当我们需要知道包中有多少元素时, 可以使用sizeof... 运算符
template<typename T, typename ... Args>
void foo(const T& t, const Args& ... rest) {
cout << sizeof...(Args) << endl;
}
16.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& ... rest) {
os << t << ", ";
return print(os, rest...);
// 此处, 每次解析rest中的第一个实参 绑定到t, 剩余的部分 继续作为rest 绑定到形参中的rest上
}
16.4.2 包扩展
即: 模板参数包/模板函数参数包 的解开操作,? 如上面的print可变参数模板函数, 当传入
print(cout, i, s, 42) 时, 模板函数将会被实例化成:?
print(ostream&, const int&, const string&, const int&);
当需要调用函数对包内元素逐个执行函数的操作时, 需要:
template <typename ... Args>
ostream& errorMsg(ostream& os, const Args& ... rest) {
return print(os, debug_rep(rest)...)
// debug_rep 也是一个模板函数, 其接受一个参数, 并打印输出参数的内容
}
16.4.3 转发参数包
转发时 定义形参为 右值版本
template<typename ... Args>
inline void StrVec::emplace_back(Args&& ... args) {
chk_n_alloc();
alloc.construct(first_free++, std::forward<Args>(args)...);
}
16.5 模版特性化
当特例化一个函数模板时, 必须为原模板中的每个模板参数提供实参. 还应在template周免跟上<>. 也就是说 此处特例化定义时 使用的类型 必须是前面定义的模板的类型相匹配.
// 特例化一个compare模板
template<>
int compare(const char* const& p1, const char* const& p2) { return strcmp(p1, p2); }
? ?template后面应该跟的typename,用于指定模板中使用的类型, 但是因为是特例化的, 本身指定了模板中使用的类型, 所以, template后面只跟<> 而未定义类型.
const char* const & 一个指向const char的const指针的引用
函数重载与模板特例化:
一个特例化版本 本质上是一个实例, 而非函数名的一个重载版本.
模板及其特例化版本应该声明在同一个头文件中, 所有同名模板的声明应该放在前面, 然后是这些模板的特例化版本. 否则编译器很难发现没有特例化声明的错误.
类模板特例化:
类模板部分特例化:
部分特例化的类 仍然是模板类
特例化成员而不是类: 只特例化成员函数而非整个模板.
template<typename T> struct Foo2 {
Foo2(const T &t = T()):mem(t) {}
void Bar();
T mem;
};
template<>
void Foo2<int>::Bar(){} // 特例化Foo<int>的成员Bar
// 特例化以后 当使用int以外的类型实例化Foo时, 跟其他情况一样
// 当使用int类型实例化Foo时, 除了Bar其余跟其他一样
|