6.2 Parameterized Types
现在我们可以将double类型的vector转换为任何类型的vector,只需要使用template替代之前几个章节中double类型。
template<typename T> class Vector { private: T* elem; int sz; public: explicit Vector(int s); ~Vector() { delete[] elem; } T& operator[](int i); const T& operator[](int i) const; int size() const { return sz; } };
此外,我们还能使用class代替typename,其含义与作用和typename相同。当我们使用template之后,其相应的成员函数也需要做出一定的修改。
template<typename T> Vector<T>::Vector(int s) { if(s < 0) throw Negative_size{}; elem = new T[s]; sz = s; } template<typename T>; const T& Vector<T>::operator[](int i) const { if(i < 0 || size() <= i) throw out_of_range{"Vector::operator[]"}; return elem[i]; } template<typename T> T* begin(Vector<T>& x) { return x.size() ? &x[0] : nullptr; } template<typename T> T* end(Vector<T>& x) { return x.size() ? &x[0] + x.size() : nullptr; }
需要注意的是,template属于编译时刻的机制,所以在运行时其并不会造成额外的开销。事实上,Vector
6.2.1 Constrained Template Arguments(C++20)
一般来说,只有当template的参数满足特点的标准,template才能生效。例如,一个Vector会进行拷贝运算,那么其潜在的标准就是其中的每一个元素都能够被拷贝。因此,我们要求Vector的template参数应该是一个Element(c++关键词),Element表示该类型的实例化对象能够成为一个元素。
template<Element T> class Vector { private: T* elem; int sz; }; Vector<int> v1; // 运行ok,因为int能够被拷贝 Vector<thread> v2; // error!!! 标准的线程不能被拷贝
在上述代码中,我们试着去实例化一个错误的template,那么就会收到编译时刻的报错。但是c++20之前的版本并不支持该特性。
6.2.2 Value Template Arguments
template除了拥有类型参数还能拥有值参数。
template<typename T, int N> struct Buffer { using value_type = T; constexpr int size() { return N; } T[N]; }; Buffer<char, 1024> glob; // 全局变量,静态分配 void fct() { Buffer<int, 10> buf; // 局部变量,存储在stack }
上述代码中的value_type与constexpr能够让用户读取到template的参数(只读)。此外,template的值类型参数在很多情况下非常有用,这样我们就能创建随机长度的Buffer,但是并不需要使用动态内存。不过需要注意的是,template的值参数必须是const expression(常量表达式)。
6.3.1 Function Templates
我们可以创建一个函数计算任何序列中的元素的总和。
template<typename Sequence, typename Value> Value sum(const Sequence& s, Value v) { for(auto x : s) v += x; return v; } void user(Vector<int>& vi, list<double>& ld, vector<complex<double>>& vc) { int x = sum(vi, 0); double d = sum(vi, 0.0); double dd = sum(ld, 0.0); auto z = sum(vc, complex{0.0, 0.0}); }
template函数可以是成员函数,但是其不能是一个虚成员函数。因为编译器无法识别程序中template的所有实例化,这样也意味着编译器无法生成vtbl。
6.3.2 Function Objects
template的另一种使用方法是function object(函数对象,或者说functor)。
template<typename T> class Less_than { const T val; // 比较对象 public: Less_than(const T v) : val{v} {} bool operator()(const T& x) const { return x < val; } }; Less_than lti {42}; Less_than lts {"Backus"s}; Less_than<string> lts2 {"Naur"}; // 因为"Naur"是C类型的string,所以我们需要<string>将其进行转换 void fct(int n, const string& s) { bool b1 = lti(n); // true,如果n<42 bool b2 = lts(s); // true,如果s<"Backus" }
上述代码中名为operator()的函数通过运算符()来进行函数调用。类型为Less_than的三个变量就是function object,其能广泛地用于函数的参数。
template<typename C, typename P> int count(const C& c, P pred) { int cnt = 0; for(const auto& x : c) if(pred(x)) ++cnt; return cnt; } void f(const Vector<int>& vec, const list<string>& lst, int x, const string& s) { cout << "number of values less than " << x << ": " << count(vec, Less_than{x}) << '\n'; cout << "number of values less than " << s << ": " << count(lst, Less_than{s}) << '\n'; }
上述代码中Less_than{x}构造了类型为Less_than
6.3.3 Lambda Expressions
上文中我们使用了Less_than来定义function object,不过还有另一种方法能够隐示地生成function object
void f(const Vector<int>& vec, const list<string>& lst, int x, const string& s) { cout << "number of values less than " << x << ": " << count(vec, [&](int a){ return a < x; }) << '\n'; cout << "number of values less than " << s << ": " << count(lst, [&](const string& a){ return a < s; }) << '\n'; }
上述函数中的[](int a){ return a < x; }被称为lambda表达式。其生成的function object类似于之前的Less_than
使用lambda表达式能够使我们的代码更为精简,但是也会使代码更为难懂。因此建议大家为lambda表达式命名,这样能够准确表达它的用处。
template<typename C, typename Oper> void for_all(C& c, Oper op) // 假设C是一组指针的容器 { for(auto& x : c) op(x); } void user2() { vector<unique_ptr<Shape>> v; while(cin) v.push_back(read_shape(cin)); for_all(v, [](unique_ptr<Shape>& ps){ ps->draw(); }); for_all(v, [](unique_ptr<Shape>& ps){ ps->rotate(45); }); }
上述代码中,我们定义了for_all函数,其用于处理所有容器的遍历操作。由于使用了unique_ptr
template<class S> void rotate_and_draw(vector<S>& v, int r) { for_all(v, [](auto& s){ s->rotate(r); s->draw(); }); }
上述代码使得lambda表达式的应用更为范例化。auto意味着任何类型都能作为参数传入,从某种程度上来说,这使得lambda表达式变为了一种template函数,我们称其为generic lambda。不过在普通的函数定义中,我们还不能将auto作为参数类型。
现在任何类型的容器都能作为rotate_and_draw的参数。
void user4() { vector<unique_ptr<Shape>> v1; vector<Shape*> v2; rotate_and_draw(v1, 45); rotate_and_draw(v2, 90); }
通过lambda表达式,我们可以将“状态”转换为表达式。以下代码是典型的初始化的过程,但显然其并不标准。对于数据结构v我们需要进行不同的操作,同时代码显得非常混乱且容易造成bug。
enum class Init_mode { zero, seq, cpy, patrn }; vector<int> v; switch(m) { case zero: v = vector<int>(n); break; case cpy: v = arg; break; }; if(m == seq) v.assign(p, q);
但是,我们可以将lambda作为一个初始化过程。
vector<int> v = [&] { switch (m) { case zero: return vector<int>(n); case seq: return vector<int>{p,q}; case cpy: return arg; } };
6.4 Template Mechanisms
为了定义优秀的template,我们还需要其他的一些工具:
- 基于类型的值:variable template
- 类型与模板(template)的别名:alias template
- 编译器选择机制:if constexpr
- 编译器“询问”类型与表达式:requires-表达式
6.4.1 Variable Templates
当我们在class template中定义一个const,我们可能希望const的值随着T的变化而变化。
template<class T> constexpr T viscosity = 0.4; template<class T> constexpr space_vector<T> external_acceleration = { T{}, T{-9.8}, T{} }; auto vis2 = 2 * viscosity<double>; auto acc = external_acceleration<float>;
6.4.2 Aliase
在标准库的头文件cstddef中包含了对于size_t的定义,例如using size_t = unsigned int。也就是说,size_t的实际类型可能会随着代码的实现而改变,在其他库中size_t的类型可能是unsigned long。因此,size_t使程序员能够更为灵活地掌控代码。
template<typename T> class Vector { public: using value_type = T; };
6.4.3 Complie-Time if
假设我们需要实现某种运算,其包含两种处理,分别为slow_and_safe(T)和simple_and_fast(T)。其通常会基于性能进行选择。传统的方式可能就是创建一个重载函数,基于标准库中的is_pod进行判断,如果是一个基类class,那么我们就进行slow_and_safe。如果是子类class,那么我们就使用重载的simple_and_fase。但是,在c++17中,我们可以使用编译器的if判断。
template<typename T> void update(T& target) { if constexpr(is_pod<T>::value) simple_and_fast(target); else slow_and_safe(target); }
6.5 Advice
[01] 对于算法表达式,使用template使其适用于大多数参数类型。
[06] 使用function object作为算法的参数。
[07] 如果你只需要在代码中的一处使用function object,那么请使用lambda。
[08] 虚函数不能为template函数。