《C++ Primer 4th》读书笔记
所谓泛型编程就是以独立于任何特定类型的方式编写代码。泛型编程与面向对象编程一样,都依赖于某种形式的多态性。
面向对象编程中的多态性在运行时应用于存在继承关系的类。我们能够编写使用这些类的代码,忽略基类与派生类之间类型上的差异。
在泛型编程中,我们所编写的类和函数能够多态地用于跨越编译时不相关的类型。一个类或一个函数可以用来操纵多种类型的对象。
面向对象编程所依赖的多态性称为运行时多态性,泛型编程所依赖的多态性称为编译时多态性或参数式多态性。
模板是泛型编程的基础。模板是创建类或函数的蓝图或公式。
函数模板
模板定义以关键字 template 开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔。模板形参表不能为空。
templateint compare(const T &v1, const T &v2){if (v1 < v2) return -1;if (v2 < v1) return 1;return 0;}
模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。类型形参跟在关键字 class 或 typename 之后定义.在函数模板形参表中,关键字 typename 和 class 具有相同含义,可以互换使用,两个关键字都可以在同一模板形参表中使用:
// ok: no distinction between typename and class in template parameter listtemplatecalc (const T&, const U&);
模板形参表示可以在类或函数的定义中使用的类型或值。使用函数模板时,编译器会推断哪个(或哪些)模板实参绑定到模板形参。一旦编译器确定了实际的模板实参,就称它实例化了函数模板的一个实例。
实质上,编译器将确定用什么类型代替每个类型形参,以及用什么值代替每个非类型形参。推导出实际模板实参后,编译器使用实参代替相应的模板形参产生编译该版本的函数。编译器承担了为我们使用的每种类型而编写函数的单调工作。
int main (){// T is int;// compiler instantiates int compare(const int&, const int&)cout << compare(1, 0) << endl;// T is string;// compiler instantiates int compare(const string&, const string&)string s1 = "hi", s2 = "world";cout << compare(s1, s2) << endl;return 0;}
函数模板可以用与非模板函数一样的方式声明为 inline。说明符放在模板形参表之后、返回类型之前,不能放在关键字 template 之前。
// ok: inline specifier follows template parameter listtemplateinline T min(const T&, const T&);// error: incorrect placement of inline specifierinline template T min(const T&, const T&);
类模板
类模板也是模板,因此必须以关键字 template 开头,后接模板形参表。Queue 模板接受一个名为 Type 的模板类型形参。
除了模板形参表外,类模板的定义看起来与任意其他类问相似。类模板可以定义数据成员、函数成员和类型成员,也可以使用访问标号控制对成员的访问,还可以定义构造函数和析构函数等等。在类和类成员的定义中,可以使用模板形参作为类型或值的占位符,在使用类时再提供那些类型或值。
templateclass Queue {public:Queue (); // default constructorType &front (); // return element from head of Queueconst Type &front () const;void push (const Type &); // add element to back of Queuevoid pop(); // remove element from head of Queuebool empty() const; // true if no elements in the Queueprivate:// ...};
与调用函数模板形成对比,使用类模板时,必须为模板形参显式指定实参:
Queue qi; // Queue that holds intsQueue< vector> qc; // Queue that holds vectors of doublesQueue qs; // Queue that holds strings
除了定义数据成员或函数成员之外,类还可以定义类型成员。如果要在函数模板内部使用这样的类型,必须告诉编译器我们正在使用的名字指的是一个类型。必须显式地这样做,因为编译器(以及程序的读者)不能通过检查得知,由类型形参定义的名字何时是一个类型何时是一个值。如果希望编译器将 size_type 当作类型,则必须显式告诉编译器这样做:
templateParm fcn(Parm* array, U value){typename Parm::size_type * p; // ok: declares p to be a pointer}
通过在成员名前加上关键字 typename 作为前缀,可以告诉编译器将成员当作类型。
如果拿不准是否需要以 typename 指明一个名字是一个类型,那么指定它是个好主意。在类型之前指定 typename 没有害处,因此,即使 typename 是不必要的,也没有关系。
非类型模板形参
模板形参不必都是类型。模板非类型形参是模板定义内部的常量值,在需要常量表达式的时候,可使用非类型形参(例如,像这里所做的一样)指定数组的长度。
// initialize elements of an array to zerotemplatevoid array_init(T (&parm)[N]){for (size_t i = 0; i != N; ++i) {parm[i] = 0;}}int x[42];double y[10];array_init(x); // instantiates array_init(int(&)[42]array_init(y); // instantiates array_init(double(&)[10]
类型等价性与非类型形参: 对模板的非类型形参而言,求值结果相同的表达式将认为是等价的。array_init 调用引用的是相同的实例—— array_init<int, 42>:
int x[42];const int sz = 40;int y[sz + 2];array_init(x); // instantiates array_init(int(&)[42])array_init(y); // equivalent instantiation
在函数模板内部完成的操作限制了可用于实例化该函数的类型。程序员的责任是,保证用作函数实参的类型实际上支持所用的任意操作,以及保证在模板使用哪些操作的环境中那些操作运行正常。
编写独立于类型的代码的一般原则:编写模板代码时,对实参类型的要求尽可能少是很有益的。说明了编写泛型代码的两个重要原则:
• 模板的形参是 const 引用。
• 函数体中的测试只用 < 比较。
通过将形参设为 const 引用,就可以允许使用不允许复制的类型。大多数类型(包括内置类型和我们已使用过的除 IO 类型之外的所有标准库的类型)都允许复制。但是,也有不允许复制的类类型。将形参设为 const 引用,保证这种类型可以用于 compare 函数,而且,如果有比较大的对象调用 compare,则这个设计还可以使函数运行得更快。
实例化
模板是一个蓝图,它本身不是类或函数。编译器用模板产生指定的类或函数的特定类型版本。产生模板的特定类型实例的过程称为实例化。模板在使用时将进行实例化,类模板在引用实际模板类类型时实例化,函数模板在调用它或用它对函数指针进行初始化或赋值时实例化。
类模板的每次实例化都会产生一个独立的类类型。为 int 类型实例化的 Queue 与任意其他 Queue 类型没有关系,对其他Queue 类型成员也没有特殊的访问权。
从函数实参确定模板实参的类型和值的过程叫做模板实参推断。
类型形参的实参的受限转换
一般而论,不会转换实参以匹配已有的实例化,相反,会产生新的实例。除了产生新的实例化之外,编译器只会执行两种转换:
• const 转换:接受 const 引用或 const 指针的函数可以分别用非 const对象的引用或指针来调用,无须产生新的实例化。如果函数接受非引用类型,形参类型实参都忽略 const,即,无论传递 const 或非 const 对象给接受非引用类型的函数,都使用相同的实例化。
• 数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当作指向其第一个元素的指针,函数实参当作指向函数类型的指针。
templateT fobj(T, T); // arguments are copiedtemplate T fref(const T&, const T&); // reference argumentsstring s1("a value");const string s2("another value");fobj(s1, s2); // ok: calls f(string, string), const is ignoredfref(s1, s2); // ok: non const object s1 converted to const reference int a[10], b[42];fobj(a, b); // ok: calls f(int*, int*)fref(a, b); // error: array types don't match; arguments aren't converted to pointers
模板实参推断与函数指针
可以使用函数模板对函数指针进行初始化或赋值,这样做的时候,编译器使用指针的类型实例化具有适当模板实参的模板版本。
templateint compare(const T&, const T&);// pf1 points to the instantiation int compare (const int&, constint&)int (*pf1) (const int&, const int&) = compare;
获取函数模板实例化的地址的时候,上下文必须是这样的:它允许为每个模板形参确定唯一的类型或值。如果不能从函数指针类型确定模板实参,就会出错。
// overloaded versions of func; each take a different function pointer typevoid func(int(*) (const string&, const string&));void func(int(*) (const int&, const int&));func(compare); // error: which instantiation of compare?
在返回类型中使用类型形参
指定返回类型的一种方式是引入第三个模板形参,它必须由调用者显式指定:
// T1 cannot be deduced: it doesn't appear in the function parameterlisttemplateT1 sum(T2, T3);// ok T1 explicitly specified; T2 and T3 inferred from argument typeslong val3 = sum (i, lng); // ok: calls long sum(int, long)
显式模板实参从左至右对应模板形参相匹配,第一个模板实参与第一个模板形参匹配,第二个实参与第二个形参匹配,以此类推。
// poor design: Users must explicitly specify all three template parameterstemplateT3 alternative_sum(T2, T1);// error: can't infer initial template parameterslong val3 = alternative_sum (i, lng);// ok: All three parameters explicitly specifiedlong val2 = alternative_sum (i, lng);
模板编译模型
编译器实例化特定类型的模板的时候,编译器必须能够访问定义模板的源代码。当调用函数模板或类模板的成员函数的时候,编译器需要函数定义,需要那些通常放在源文件中的代码。标准 C++ 为编译模板代码定义了两种模型。
在包含编译模型中,编译器必须看到用到的所有模板的定义。一般而言,可以通过在声明函数模板或类模板的头文件中添加一条 #include 指示使定义可用,该#include 引入了包含相关定义的源文件:
// header file utlities.h#ifndef UTLITIES_H // header gaurd (Section 2.9.2, p. 69)#define UTLITIES_Htemplateint compare(const T&, const T&);// other declarations#include "utilities.cc" // get the definitions for compare etc.#endif// implemenatation file utlities.cctemplate int compare(const T &v1, const T &v2){if (v1 < v2) return -1;if (v2 < v1) return 1;return 0;}// other definitions
在分别编译模型中,编译器会为我们跟踪相关的模板定义。但是,我们必须让编译器知道要记住给定的模板定义,可以使用 export 关键字来做这件事。对类模板使用 export 更复杂一些。通常,类声明必须放在头文件中
// class template header goes in shared header filetemplateclass Queue { ... };// Queue.ccimplementation file declares Queue as exportedexport template class Queue;#include "Queue.h"// Queue member definitions
导出类的成员将自动声明为导出的。也可以将类模板的个别成员声明为导出的,在这种情况下,关键字 export 不在类模板本身指定,而是只在被导出的特定成员定义上指定。
类模板的 static 成员
templateclass Foo {public:static std::size_t count() { return ctr; }// other interface membersprivate:static std::size_t ctr;// other implementation members};
每个实例化表示截然不同的类型,所以给定实例外星人所有对象都共享一个static 成员。因此,Foo<int> 类型的任意对象共享同一 static 成员 ctr,Foo<string> 类型的对象共享另一个不同的 ctr 成员。
通常,可以通过类类型的对象访问类模板的 static 成员,或者通过使用作用域操作符直接访问成员。当然,当试图通过类使用 static 成员的时候,必须引用实际的实例化:
Foo fi, fi2; // instantiates Foo classsize_t ct = Foo ::count(); // instantiates Foo ::countct = fi.count(); // ok: uses Foo ::countct = fi2.count(); // ok: uses Foo ::countct = Foo::count(); // error: which template instantiation?
与任意其他成员函数一样,static 成员函数只有在程序中使用时才进行实例化。
像使用任意其他 static 数据成员一样,必须在类外部出现数据成员的定义。在类模板含有 static 成员的情况下,成员定义必须指出它是类模板的成员:
template <class T> size_t Foo<T>::ctr = 0; // define and initialize ctr
一个泛型句柄类
/* generic handle class: Provides pointerlike behavior. Although access through* an unbound Handle is checked and throws a runtime_error exception.* The object to which the Handle points is deleted when the last Handle goes away.* Users should allocate new objects of type T and bind them to a Handle.* Once an object is bound to a Handle,, the user must not delete that object.*/templateclass Handle {public:// unbound handleHandle(T *p = 0): ptr(p), use(new size_t(1)) { }// overloaded operators to support pointer behaviorT& operator*();T* operator->();const T& operator*() const;const T* operator->() const;// copy control: normal pointer behavior, but last Handle deletes the objectHandle(const Handle& h): ptr(h.ptr), use(h.use){ ++*use; }Handle& operator=(const Handle&);~Handle() { rem_ref(); }private:T* ptr; // shared objectsize_t *use; // count of how many Handle spointto *ptrvoid rem_ref(){ if (--*use == 0) { delete ptr; delete use; } }}; template inline Handle & Handle ::operator=(const Handle &rhs){++*rhs.use; // protect against self-assignmentrem_ref(); // decrement use count and delete pointers ifneededptr = rhs.ptr;use = rhs.use;return *this;} template inline T& Handle ::operator*(){if (ptr) return *ptr;throw std::runtime_error("dereference of unbound Handle");}template inline T* Handle ::operator->(){if (ptr) return ptr;throw std::runtime_error("access through unbound Handle");} template inline const T* Handle ::operator->() const{ if (ptr) return ptr;else throw std::logic_error("unbound Sales_item"); } template inline const T& Handle :: const{ if (ptr) return *ptr;else throw std::logic_error("unbound Sales_item");} class Sales_item {public:// default constructor: unbound handleSales_item(): h() { }// copy item and attach handle to the copySales_item(const Item_base &item): h(item.clone()) { }// no copy control members: synthesized versions work// member access operators: forward their work to the Handle classconst Item_base& operator*() const { return *h; }const Item_base* operator->() const{ return h.operator->(); }private:Handle h; // use-counted handle};
模板特化
模板特化(template specialization)是这样的一个定义,该定义中一个或多个模板形参的实际类型或实际值是指定的。特化的形式如下:
• 关键字 template 后面接一对空的尖括号(<>);
• 再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参;
• 函数形参表;
• 函数体。
templateint compare(const T &v1, const T &v2){if (v1 < v2) return -1;if (v2 < v1) return 1;return 0;} // special version of compare to handle C-style character stringstemplate <>int compare (const char* const &v1,const char* const &v2){return strcmp(v1, v2);}
模板特化必须总是包含空模板形参说明符,即 template<>,而且,还必须包含函数形参表。如果可以从函数形参表推断模板实参,则不必显式指定模板实参:
// error: invalid specialization declarations// missing template<>int compare(const char* const&,const char* const&);// error: function parameter list missingtemplate<> int compare ;// ok: explicit template argument const char* deduced from parameter typestemplate<> int compare(const char* const&, const char* const&);
当定义非模板函数的时候,对实参应用常规转换;当特化模板的时候,对实参类型不应用转换。在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配,如果不完全匹配,编译器将为实参从模板定义实例化一个实例。
与其他函数声明一样,应在一个头文件中包含模板特化的声明,然后使用该特化的每个源文件包含该头文件。
普通作用域规则适用于特化
在能够声明或定义特化之前,它所特化的模板的声明必须在作用域中。类似地,在调用模板的这个版本之前,特化的声明必须在作用域中:
// define the general compare templatetemplateint compare(const T& t1, const T& t2) { /* ... */ }int main() {// uses the generic template definitionint i = compare("hello", "world");// ...}// invalid program: explicit specialization after calltemplate<>int compare (const char* const& s1,const char* const& s2){ /* ... */ }
这个程序有错误,因为在声明特化之前,进行了可以与特化相匹配的一个调用。当编译器看到一个函数调用时,它必须知道这个版本需要特化,否则,编译器将可能从模板定义实例化该函数。
重载与函数模板
函数模板可以重载:可以定义有相同名字但形参数目或类型不同的多个函数模板,也可以定义与函数模板有相同名字的普通非模板函数。
如果重载函数中既有普通函数又有函数模板,确定函数调用的步骤如下:
1. 为这个函数名建立候选函数集合,包括:
a. 与被调用函数名字相同的任意普通函数。
b. 任意函数模板实例化,在其中,模板实参推断发现了与调用中所用函数实参相匹配的模板实参。
2. 确定哪些普通函数是可行的(第 7.8.2 节)(如果有可行函数的话)。候选集合中的每个模板实例都 可行的,因为模板实参推断保证函数可以被调用。
3. 如果需要转换来进行调用,根据转换的种类排列可靠函数,记住,调用模板函数实例所允许的转换是有限的。
a. 如果只有一个函数可选,就调用这个函数。
b. 如果调用有二义性,从可行函数集合中去掉所有函数模板实例。
4. 重新排列去掉函数模板实例的可行函数。
• 如果只有一个函数可选,就调用这个函数。
• 否则,调用有二义性。
设计既包含函数模板又包含非模板函数的重载函数集合是困难的,因为可能会使函数的用户感到奇怪,定义
函数模板特化几乎总是比使用非模板版本更好。