您的位置:首页 > 编程语言 > C语言/C++

《C++ Primer (5th Edition)》笔记-Part III . Tools For Class Authors

2013-10-03 15:22 267 查看
注:本文以《C++ Primer(英文版)》(5th Edition)为参考。

总共由四部分组成:

《C++ Primer (5th Edition)》笔记-Part I. The Basics

《C++ Primer (5th Edition)》笔记-Part II. The C++ Library

《C++ Primer (5th Edition)》笔记-Part III. Tools For Class Authors

《C++ Primer (5th Edition)》笔记-Part IV. Advanced Topics



Part III. Tools For Class Authors

Chapter 13. Copy Control

注:本文以《C++ Primer(英文版)》(5th Edition)为参考。

13.1 与拷贝控制相关的5个函数(3类:拷贝构造、赋值、析构):copy constructor、move constructor、copy-assignment operator、move-assignment operator、destructor。这5个函数,再加上default constructor函数(共6个函数)是可能被自动合成的。这里,move constructor、move-assignment operator是C++11标准引入的新内容。

13.2 拷贝构造的参数通常已改写成const reference的形式。如果参数不是引用传递,而是值传递,会有什么后果?(编译不过)。

13.3 我们已经知道,初始化有两种:direct initialization和copy initialization。在class上,direct initialization就是直接调用构造函数,这个没有疑问;copy initialization通常是应该调用copy constructor或者move constructor。但是,大多数编译器会跳过copy constructor的,即把string str = "abc"重写为string str("abc")。这是允许,但是编译器没有义务这么做。尽管编译器可以跳过copy/move
constructor,但是,在这样做的时候,copy/move constructor必须存在。(在VS2012测试,好像没有这个限制)。

13.4 copy-assignment operator通常应该返回引用类型,参数为常量引用类型。

13.5 在构造函数体执行之前,成员变量会以类内声明的顺序被初始化;析构正好相反,先执行析构的函数体,然后成员变量以与声明顺序相反的顺序被销毁。

13.6 合成的析构函数,函数体为空,且不是虚函数。

13.7 这三类控制函数通常应该被作为一整体。rule of three(三法则、实际上,现在应该是rule of five,五法则):classes that need destructors need copy and assignment。另外还有一点:classes that need copy need assignment, and vice versa。

13.8 C++11中可以对使用=default 和=delete,一个deleted函数,是一个被声明过,但是永远不能不使用的函数。=default可以出现在声明处或者定义处(不能写函数体,包括大括号),但只能用在default constructor或者copy-control成员上;=delete只能出现在声明处,但是其可以用在任意的成员函数上。用了=default或者=delete之后,都不能再定义函数体,否则编译出错。

13.9 析构函数不应该被声明为deleted函数,否则,我们将无法定义变量(编译出错),虽然我们可以动态申请该类型的变量,但是不能销毁(编译出错),当析构函数为private时,也会出现同样的情况。

struct NoDtor {
NoDtor() = default; // use the synthesized default constructor
~NoDtor() = delete; // we can't destroy objects of type NoDtor
};
NoDtor nd; // error: NoDtor destructor is deleted
NoDtor *p = new NoDtor(); // ok: but we can't delete p
delete p; // error: NoDtor destructor is deleted

13.10 自动将default constructor或者copy-control成员defined为deleted函数的情况如下:(数组对赋值操作符、默认构造函数没有影响)

The synthesized destructor is defined as deleted if the class has a member whose owndestructor is deleted or is inaccessible (e.g., private).
The synthesized copy constructor
is defined as deleted if the class has a member whose own copy constructoris deleted or inaccessible. It is also deleted if the class has a member with a deleted or inaccessibledestructor.
The synthesized copy-assignment operatoris defined as deleted if a member has a deleted or inaccessiblecopy-assignment operator, or if the class has
aconst orreference member.
The synthesized default constructor is defined as deleted if the class has a member with a deleted or inaccessibledestructor; or has areference
member that does not have an in-class initializer (§ 2.6.1, p. 73); or has aconst member whose type does not explicitly define a default constructor and that member does not have an in-class initializer.

总结如下:1)成员变量相应的copy-control成员定义为deleted或者不可访问时;2)当成员变量的destructor为deleted或者不可访问时,构造函数(默认构造和拷贝构造)和析构函数都为=delete;3)当成员变量有const或者reference成员变量时,copy-assignment operator为=delete,若这些变量没有in-class initializer,default constructor也为=delete。另外需要注意,当class内显式声明了constructor(即使声明为=delete的构造函数,如:A(const
A&)=delete),编译器都不会再合成default constructor了,注意,这里是“不合成”不是“合成为=delete”。另有关于move的补充说明:见15.17。

13.11在C++11之前,阻止拷贝的办法的:声明为private,并且不定义函数。显式调用会有编译错误,frind或者成员函数调用有链接错误。现在,建议用=delete,调用总是编译错误。

13.12 为了管理资源,除了定义copy-control成员,还经常需要定义swap函数(需要做深拷贝的class,在swap时,往往只需要浅拷贝)。关于swap的定义,要参考《Effective C++(third edition)》中的“条款25:考虑写出一个不抛出异常的swap函数”:1)在类内部定义一个不会抛出异常的swap函数;2)如果你定义的是class,那么在类外全特化std::swap,在函数内部调用类的swap成员函数;3)如果你定义的class template,在类所在命名空间内,定义一个非成员swap模板函数,此函数调用class
template内的swap成员函数;4)在使用swap前,包含一个using声明,让std::swap在此调用前曝光,然后不加任何命名空间限定符,赤裸裸地调用swap函数(这里要用到Part IV,18.14中的名字查找规则),这样就会优先调用我们自定义的分成员swap函数。另外,个人认为,在C++11标准下,如果我们定义了move copy和move-assignment operator 之后,就不用再考虑自己定义swap的问题了。

13.13 copy and swap技术。参数为值传递,然后调用swap函数。这种技术能handle自身赋值,并且是异常安全(exception safe)的。

A& A::operator=(A ai){
using std::swap;
//swap the contents of the left-hand operand with the local variable rhs
swap(*this, ai); // rhs now points to the memory this object had used
return *this; // rhs is destroyed, which deletes the pointer in rhs
}

13.14 C++11引入了move语义:主要针对拷贝之后,立即销毁的对象。这样,对不能拷贝的类(如:IO,unique_ptr),就能通过move来传递。C++11之前,只有copyable的类才能放在容器中,在C++11中,只要是movable的类,就可放在容器中了。所以IO,unique_ptr是能够放在容器中的(在GCC4.8.1中测试,对容器对IO支持的还不是太好)。

13.15 为了配合Move语义,C++11中引入了右值引用类型,像任何引用类型一样,右值引用只是对象的另外一个名字。右值引用只能绑定到右值上,如int&&rr = 34, &&rr2 = i * 23。由于右值只能是literals或者临时对象,所有右值引用所引用的对象即将被销毁,即Lvalues persist.Rvalues are ephemeral。另外,一个变量就是一个只有1个操作数没有操作符的表达式,且变量表达式是左值,即Variables are lvalues(另一个角度:Rvalues
are ephemeral,变量却persist until it goes out of scope,所以变量是左值)!所以,右值引用类型变量是左值,我们不能将右值引用绑定到右值引用变量上。int &&rr1 =42; int &&r2 = rr1;//error。虽然右值引用不能直接绑定在左值变量上,但可以通过std::move()来将左值转化为对应的右值引用类型,左值在被std::move调用之后,除了被赋值和被销毁,不能做其他操作。注意,这里一定要用std::move,不是move,命名空间限定符不能丢。

13.16 move constructor 和 move assignment operator都不能抛出异常,应该标记为noexcept,必须同时出现(或都不出现)在声明和定义处。参数类型为A&&,(通常)不用加const(若加了const,s将不法修改)。

StrVec::StrVec(StrVec &&s) noexcept // move won't throw any exceptions
// member initializers take over the resources in s
: elements(s.elements), first_free(s.first_free), cap(s.cap)
{
//leave s in a state in which it is safe to run the destructor
s.elements = s.first_free = s.cap = nullptr;
}


13.17 注意在定义(move)assignment operator时,要谨慎处理自赋值的情况,通常使用this==&v来加以判断。

13.18 与其他copy-control成员不同,对于一些类,编译器根本不会合成move copy-control成员(其他copy-control函数,在不能合成时,就会自动定义为deleted,而对move成员函数,是不声明)。只有在类中没有定义任何copy-control成员函数,并且每个非静态数据成员都是movable的时候,编译器才会合成move copy-control 成员。

13.19 根据13.18,编译器不会自动合成出=delete的move copy-control成员。但是有一种例外:就是显式声明move copy-control为=default,但是编译器move所有非静态成员时,才会将显式声明的mov copy-control重新定义为=delete。具体情况为:1)成员的对应的move copy-control没有定义、定义不可见、定义为=delete;2)move copy constructor: 某个数据成员的析构函数为deleted或不可见;3)move
copy-assignment operator:某个数据成员有const或者reference数据成员。另有补充说明:见15.17。

13.20 当类中定义了move constructor move-assignment operator,那么,编译器合成的copy constructorcopy-assignment operator 都会被定义为deleted。注意,这条规则与13.18不是完全对称的,析构函数在其中是个例外。这里的“定义”都是指显式定义,显式声明A(A
&&ai) = delete,也相当于显式定义。

13.21 A&&类型是能够转换为const A&类型的。所以如果类中没有要调用的move copy-control,但有对应的非move copy-control时,会自动调用非move版本,这要做时绝对安全的。

13.22 C++11中引入了move iterator。通常解引用iterator得到的是左值引用,但是解引用move iterator得到的将是右值引用。通过函数make_move_iterator(iterator)来将其他iterator变为move iterator。注意,C++标准库,并不保证哪些算法可以用move iterator,哪些不能,所以使用要谨慎,在充分confidant的情况下使用。

13.23 如果定义重载函数来区分moving和copying,通常定义为一个参数为const T&另一个参数为T&&。注意:通常将参数定义为T&&而不是const T&&,个人觉得,主要是因为我们接触的大部分复制后即销毁的变量(包括没名字的临时变量)都不是const。

void push_back(const T&);//copy:binds to any kind of T;
void push_back(T&&);	//move:binds only to modifiable rvalues of type T
//=====测试代码=====
void f(int &&t);
void f(const int &t);
int i = 32;
const int ci = 32;
f(43);			//call f(int&&)
f(std::move(i));//call f(int&&)
f(std::move(ci));//call f(const int&)
f(ci);			//call f(const int&)


13.24 在C++11以前,不论对象是右值还是左值,都可以调用它的成员函数。在C++11中,可以像使用const限定符一样,在参数列表后面跟上“&”或者“&&” reference qualifier,来限定this指针的左值/右值性。&限定的成员函数,只有通过左值调用,&&限定的则只能通过右值调用。跟const、noexcept一样,reference qualifier必须同时出现在声明和定义处。三者的顺序是const、reference qualifier、noexcept,不能反掉。

13.25 当我们定义const成员函数时,我们可以定义两个(相同的名字,“相同”的参数列表),一个带const,一个不带const。但是reference qualifier没有默认值,当类内有多个相同名字,相同参数列表的成员函数时,这些函数必须都带或者都不带reference qualifier。

Chapter 14. Overloaded operation and conversions

14.1 一个operator函数,必须是类的成员函数,或者至少有一个类类型的参数。

14.2



14.3 保证evaluated顺序的操作符(如逻辑操作符、逗号操作符)在重载后就不保证evaluation顺序了,另外,逻辑操作符重载还有,也不具有短路功能了,两边的操作数都会被evaluated。另外,对于逗号操作符,和取地址操作符,语言定义已经定义了对类对象的意义,即所有对象有built-in meaning。因此,尽量不要重载这些操作符。

14.4 重载后的操作符的意义要跟built-in的meaning保持一致。

14.5 对重载函数的指导原则:1)=,[],(),->必须定义为成员函数;2)compound-assignment操作符通常ought to be members,但不是必须;3)需要改变类的状态,或者closely tied to their given type,(例如:++,--,*(解引用操作符))should be members。3)对称的操作符(如算术、相等、关系、位操作符)should be 非成员函数。4)IO操作符、涉及混合类型的对称操作符必须为非成员函数。

14.6 若操作符为成员函数,那么其左操作数必须为其所属类类型(P556. 疑问:解引用操作符的操作数时在右边的呀)。例如:string s = "cdef"; string s2 = "ab" + s;编译出错。

14.7 IO操作符应该返回流的引用。

14.8 前自增,前自减:A& operator++(); A& operator--(); 后自增、后自减:A& operator++(int), A& operator--(int)。

14.9 解引用操作符应该返回类型引用。箭头操作符必须返回类型对象的地址,或者一个定义了箭头操作符的类对象。

14.10 重载了function-call operator的对象,叫做function object。Lambdas are function objects。但是Lambda的函数调用操作符是const成员函数,只用用mutable修饰的Lambda的调用操作符才不是const的(P573. 疑问:没用mutable修饰的Lambda是能够修改capture list里的引用对象的)。

14.11 library已经定义的函数对象:



14.12 C++11定义了function类型:function<int(int, int)>。来解决同一个call signature,但不同callable object的问题。function对象不能用重载函数的函数名来赋值。

int add(int i, int j) { return i + j; }
Sales_data add(const Sales_data&, const Sales_data&);
map<string, function<int(int, int)>> binops;
binops.insert( {"+", add} ); // error: which add?
int (*fp)(int,int) = add; 	// pointer to the version of add that takes two ints
binops.insert( {"+", fp} ); // ok: fp points to the right version of add
// ok: use a lambda to disambiguate which version of add we want to use
binops.insert( {"+", [](int a, int b) {return add(a, b);} } );

14.13 conversion operator:operator type() const;C++11引用的explicit conversion operator: explicit operator type() const。由于到bool类型的转化操作符,通常是用在条件表达式里面的,所以,operator bool通常应该定义为explicit。

14.14 在Part I的6.22和6.23中,分析了函数匹配,参数类型转换的优先级。当两个函数都用到相同user-defined conversion时,需要再考虑附加的标准转化(非user-defined conversion);当两个函数都用到不同user-defined conversion时,那么这两函数调用就是ambiguous(不再考虑标准转换)。个人,认为unser-defined conversion即class-type conversion,一般通过构造函数或重载类型转换操作符实现;另外,only
one class-type conversion is allowed!。

Chapter 15. Object-oriented Programming

15.1 面向对象编程的三个基本理念:数据抽象、继承、多态。

15.2 作为基类,通常都是应该将析构函数定位为virtual,可以这样声明:virtual ~A() = default;。注意:构造函数,内联成员函数,静态成员函数都不能为虚函数(成员模板也不能为虚函数)。

15.3 在derived类的override(覆盖)函数前,可以加virtual关健字,但不是必须的,一个成员成员声明为了virtual,他在derived classes都一直保持virtual。

15.4 如果基类内定义了static member,那么整个hierarchy都只存在这一个成员。静态成员是遵循access control的,如果derived类能够访问,那么我们可以通过父类或子类都可以使用此static 成员。

15.5 一个类被作为基类时,这个类必须已被定义,不能仅仅声明。

15.6 在C++11标准中,可以在类的定义时声明为final,来阻止被继承。

class NoDerived final{/**/};
class Base{/**/};
class NoDerived2 final : Base{/**/};

15.7 当把子类对象(不通过指针、引用)赋值给父类对象时,子类会被sliced down;父类到子类没有隐式转换。

15.8 当子类的成员好override父类的virtual函数,那么他们参数列表必须一致,函数的返回类型通常也应该是一致的,但是有一个例外:允许virtual functions各自返回自身类型的引用(或指针),但是,这是在derived-to-base conversion是accessible的前提下(见15.14)。

15.9 在C++11中,引入了override关键字,来显式地覆盖(要求编译器检查一致性)。另外,我们也可以将virtual函数声明为final,子类中试图ovrride此virtual函数都是错误的。final 和 override都只能修饰virtual函数。

15.10 如果virtual函数有默认实参,那么在基类和子类的virtual函数中默认实参应该保持一致。

15.11 我们可以用scope operator来阻止动态绑定。

//calls the version from the base class regardless of the dynamic type of baseP
double undiscounted = baseP->Quote::net_price(42);

15.12 虚函数如果声明为=0,那么该虚函数就称为pure virtual function。与普通的虚函数不同,纯虚函数可以只声明不定义(但也可以定义)。具有纯虚函数的类被称为抽象基类。=0,override,final等关键在都应放在noexcept关键字之后。const | reference qualifier | noexcept | final、override、=0。

15.13 公有继承:权限不变;保护继承:变保护;私有继承:变私有。

15.14 derived-to-base conversion(假设D继承于B):1)只有在D公有继承B的前提下,用户代码才能够使用derived-to-base conversion;2)不论D以什么方式继承B,D的成员函数或者友元,能够使用derived-to-base conversion;3)如果D公有继承或者保护继承B,那么D的子类的成员函数或者友元,能够使用derived-to-base conversion。若一个drived类有多个基类(多层次继承、多继承),在函数匹配(重载)时,derived类向每个基类转换的优先级是一样的,但derived-to-base
conversion与其他conversion相比的优先级大小,C++ Primer中未有提及。

15.15 friendship is not inherited。

15.16 可以使用using declaration,修改(直接或间接)基类成员name的access权限。 除了覆盖虚函数,子类不应该定义与(直接或间接)基类成语name相同的成员(尽管参数列表可能不一样)。由于Name lookup发生在type checking之前,如果子类定义了相同的name,会隐藏(直接或间接)基类的成员。

15.17 除了13.10 列出的引起copy-control为=delete的情况,13.19列出的导致move copy-control=delete情况外,如果基类没有相应copy-control(或为delete、或不可访问)、基类的destructor为deleted(或不可访问)也会导致应用相应的函数为=delete。

15.18 注意子类的move copy constructor的参数传递方式(右值引用再次作为实参传入时,需要使用std::move()):

D(const D &d):Base(d){/**/}
D(D &&d):Base(std::move(d)){/**/}
D& operator=(const D &d){
Base::operator=(d);
return *this;
}


15.19 与其他copy-control函数不同,destructor只负责自己申请的资源的销毁工作,不负责基类的(由编译器负责)。

15.20 在构造函数或者析构函数中调用虚函数是没有意义的(P.627),这种行为,被编译器定向为调用自身的相应函数。

15.21 C++11标准中引入了Inherited Constructor(继承构造函数)。使用形式为通过using declaration:using Base::Base;Inherited Constructor有以下特性:

1)只能继承直接基类构造函数。

2)与普通成员的using declaration不同,不管constructor的using declaration在什么(access control)位置,继承来的构造函数的访问权限与基类总是保持一致,基类private的构造函数,继承后也是private(疑问:private也可以继承?);protect 和public的情况与此相同。

3)using declaration不能加explicit或者constexpr限定符,继承来的构造函数的explicit、constexpr属性与基类保持一致。

4)如果基类的构造函数有默认实参,那么子类将会得到多个构造函数,根据默认参数的个数,参数依次减少。

5)通常子类会继承父类的所有构造函数,但是也有例外:a、如果继承来的构造函数与自己定义的构造函数参数列表一样,则不继承;b、默认、拷贝、move构造函数均不被继承。

6)使用using declaration得到的构造函数,不是user-define的构造函数,所以,如果一个类仅有继承构造函数,那么编译器会合成默认构造函数(以及其他copy-control函数)

Chapter 16 Templates and Generic Programming

16.1 在模板的定义中,template parameter list(模板参数列表)不能为空。模板参数可以为“types”、“values”两种类型。“type"类型的模板参数用typename或者class修饰,”value“类型的模板参数,用具体的类型修饰。在实例化模板时,用来初始化”value“型模板参数的实参,必须为const expression(那么,用指针或者引用初始化时,必须有static的生命周期)。

template <typename T> T foo(T* p){/**/}
template <class T> T foo(T* p){/**/}
template<unsigned N,unsigned M>
int compare(const char (&p1)
, const char (&p2)[M]){
return strcmp(p1, p2);
}


16.2 函数模板可以是inline或者constexpr的。这两个限定符不许出现在模板参数列表之后,函数返回类型之前:

// ok: inline specifierfollows the template parameter list
template <typename T> inline T min(const T&, const T&);
// error: incorrect placement of the inline specifier
inline template <typename T> T min(const T&, const T&);

16.3 函数模板、类模板的成员函数(模板)通常定义在头文件中。

16.4 与函数模板不同,类模板不能够推演模板参数,必须显式指定。By default,在实例化类模板时,只有用到的成员函数才会被实例化。类模板的成员函数的定义方式如下所示。

template <typenameT>
ret-type Blob<T>::member-name(parm-list)

16.5 在类模板内部使用类名是不用指出模板参数,但在外部必须写出。

template <typename T>
class A{
public:
A(){}
A& f(A &ai);
};
template <typename T>
A<T>& A<T>::f(A &ai){
return *this = ai;
}

16.6 与Part I中7.13小项的类的友元的情况相比,(模板)类的模板友元定义要相对复杂一点。一个(模板)类要将另一个模板作为友元,

1)1对1类型的。友元是函数模板或者类模板,需要(前向)声明。

// forwarddeclarations needed for friend declarations in Blob
template <typename> class BlobPtr;
template <typename> class Blob; // needed for parameters in operator==
template <typename T> bool operator==(const Blob<T>&, const Blob<T>&);
template <typename T> class Blob {
friend class BlobPtr<T>;
friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
};

2) 1对多时。友元是函数模板或者类模板,不需要(前向)声明。

//template <typename> class BlobPtr;
//template <typename T> bool operator==(const T&, const T&);
template <typename T> class Blob {
template <typename X>
friend class BlobPtr;
template <typename X>
friend bool operator==(const Blob<T>&, const Blob<T>&);
};

16.7 在C++11标准下,可以将自己的type模板参数声明为友元。注意,尽管具体实例化时,Type为built-in类型也没有关系。

template <typename Type> class Bar { friend Type;};

16.8 由于模板不是类型,所以不能用typedef定义模板的别名。但在C++11标准下,可以用using定义模板的别名。

template<typename T> using twin = pair<T, T>;
twin<string> authors; //authors is a pair<string, string>
template <typename T> using partNo = pair<T, unsigned>;
partNo<string> books;  //books is a pair<string, unsigned>

16.9 如果模板类里面有静态成员,那么该模板类的每个实例化类都有一个自己的静态成员。与普通类的静态成员类似(见Part I中7.27小项),每个实例类的静态数据成员只能(而且需要)被定义一次,定义方式如下代码所示。与模板类的其他成员函数一样,静态成员函数只有在用到的时候才会被实例化。
template <typenameT>
size_t Foo<T>::ctr = 0; // define and initialize ctr

16.10 与普通函数的参数用法一样,函数模板的参数也可以不写名字(此时没法使用),模板声明中和定义中的模板参数的名字可以不一样。

16.11 通常情况下class和typename的可以相互替换,但有一个例外:当要告诉编译,某个名字是一个type时,只能用typename,不能用class。

16.12 在以前的标准中,只允许对类模板的模板参数提供默认值,但在C++11标准下,也可以对函数模板的参数提供默认值。对于类模板,当所有模板参数都有默认值时,实例化类模板时就可以不用给出实参了,但是尖括号不不能丢掉的。

template<typename T = int> class A{/**/};
A<> ai;

16.13 类(模板)成员函数可能自身就是一个模板函数,这类成员叫做member templates(成员模板)。成员模板不能为虚函数。在调用成员模板时,可以进行参数推演,也可以显式指定。(疑问:显式指定时在函数名后面跟实参列表即可,但如果构造函数是成员模板呢?没地方放模板实参列表了,难道构造函数作为成员模板在调用是只能参数推演?)。shared_ptr、unique_ptr等的构造函数都有模板类型的,这样就可以处理指针间的隐性转换问题。

16.14 类模板的的成员模板在类外定义时,应该先写类模板的参数列表,然后再写成员函数的模板参数列表。

template <typenameT> class Blob {
template<typename It> Blob(It b, It e);
};
template <typenameT>  	// type parameter for the class
template <typename It>  // type parameter for the constructor
Blob<T>::Blob(Itb, It e){/**/}


16.15 当不同编译单元都实例化同一类型的模板,那么,每个文件都有想用的模板实例。为了避免重复实例化,C++11引入了”explicit instantiation"。一个模板实例,可以有多次(带extern)声明,但只能定义一次。

extern template declaration;// instantiation declaration
template declaration;		// instantiation definition
extern template class Blob<string>;  // declaration
template int compare(const int&, const int&);  // definition

16.16 在Part II中的12.9小项中,已经提到过shared_ptr和unique_ptr的区别。在shared_ptr中,deleter可以作为参数传递,而且类型不固定,所以deleter肯定不能是shared_ptr的成员,shared_ptr应该通过动态绑定(多态)的方式来支持自定义deleter的;在unique_ptr中,由于实例化是就知道deleter的类型,所以unique_ptr内部可以保存是deleter(相关)的callable object作为成员。具体分析见P.676。

16.17 与普通函数不同(见Part I的6.23小项),当让编译器做参数推演时,函数模板允许的模板type参数的conversion是很有限的:1)top-level const可以被忽略掉,2)const coversions:noncost reference(or pointer)to const reference(or pointer),3)数组(或函数)到指针类型的转换。注意:如果显式指定了全部的模板type参数的类型,那么coversion规则就和普通函数一样了。

template <typename T> T fobj(T, T); // arguments are copied
template <typename T> T fref(const T&, const T&); // references
string s1("a value");
const string s2("another value");
fobj(s1, s2); // calls fobj(string, string); const is ignored
fref(s1, s2); // calls fref(const string&, const string&)
// uses premissible conversion to const on s1
int a[10], b[42];
fobj(a, b); // calls f(int*, int*)
fref(a, b); // error: ambiguous int [42] or int [10] ?

16.18 explicit template argument:按照从左到右的顺序依次匹配,如果argument个数小于parameter个数,那么右边剩余的parameter需要编译器推演 。

template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
// T1 is explicitly specified; T2 and T3 are inferred from the argument types
auto val3 = sum<long long>(i, lng); // long long sum(int, long)

16.19 使用C++11的Trailing Return特性,能够很好地解决返回类型为问题:

// a trailing return lets us declare the return type after the parameter list is seen
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg){
// process the range
return *beg; // return a reference to an element from the range
}

16.20 在type_trait头文件夹下,定义了许多“type transformation"类模板(不是函数模板),这些模板主要要来做metaprogramming(元编程)。





16.21 将函数模板初始化(或者赋值给)一个函数指针时,编译会根据函数指针的类型来推演模板参数:

template <typename T> int compare(const T&, const T&);
// pf1 points to the instantiation int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;
// overloaded versions of func; each takes a different function pointer type
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare); // error: which instantiation of compare?
func(compare<int>); // ok!passing compare(const int&, const int&)

16.22 如果函数模板为:template <typename T> void f(T &p);那么函数只能传入左值,如果函数模板的函数参数类型为const T&,那么函数可以传入任意类型的实参。通常,我们无法直接定义reference to reference,但是我们可以通过类型别名、模板type参数来间接定义;C++11之前,这种情况统一collapse为左值引用类型,在C++11中,为了增加对右值引用的支持,扩展了collapsing rules。given type X,那么:X& &,X& &&,and
X&& & all collapse to type X&;只有X&& &&类型collapse to X&&。所以对于函数模板:template <typename T> void f(T &&p);当传入右值时,按正常规则推演,当传入左值时,T的类型为左值引用,参数也变为左值引用类型,如int i = 9; f(i);则T为int&类型,将实例化函数template <> void f<int &> (int &p)。所以,当函数模板的函数参数类型为T&&时,我们可以传入任意类型的实参(注意,这只针对函数模板,并不适用于普通函数)。

16.23 通常,函数模板的函数参数不定义为右值引用,但是两种情况例外:1)模板是为了forwarding他的实参,2)定义重载模板了。重载时的情况与13.23小项中的说明类似,一般定义为:

template <typename T> void f(T&&); // binds to nonconst rvalues
template <typename T> void f(const T&); // binds to lvalues and const rvalues

16.24 std::move的定义是很巧妙的。对于string右值,实例化为string &&mov(string &&),对于string左值,实例化为string&& mov(string&)。内部调用的是static_cast,C++是允许通过static_cast来将左值转为右值引用的。

template <typename T>
typename remove_reference<T>::type&& move(T&& t){
return static_cast<typename remove_reference<T>::type&&>(t);
}

16.25 Forwarding。当模板函数F要将参数再次传给函数f时:当f的参数都是值类型时,定义1是正确的;当f的参数还可能是引用类型时,定义2可以handle;当f的参数还可能是右值引用类型时,(根据变量都是左值原则13.15)定义2就不能handle了,只能用定义3的形式了。std::forward是C++11新引入的库函数,与其他函数模板不一样,std::forward必须显式指定出模板参数,当函数模板参数为T&&类型,由于std::forward<T>返回的是T&&类型,所以其能够保留实参的一切细节(注意,std::forward应该跟T&&的函数模板的函数参数类型配合使用)。

template <typename F, typename T1, typename T2>
void F(F f, T1 t1, T2 t2){//定义1,ok for f(A a, A b)
f(t2, t1);
}
template <typename F, typename T1, typename T2>
void F(F f, T1 &&t1, T2 &&t2){//定义2,ok for f(A &a, A &b)
f(t2, t1);//
}
template <typename F, typename T1, typename T2>
void F(F f, T1 &&t1, T2 &&t2){//定义3,ok for f(A &&a, A &&b)
f(std::forward<T1>(t2), std::forward<T1>(t1));
}

16.26 模板函数参与的重载(匹配)。对Part I的6.22、6.23小项中对普通函数的匹配法则,以及conversion的优先级做了说明,在14.14小项中对class-type conversion做了更细的说明,在16.17中对template的conversion做了说明。在函数匹配中,尽管有函数模板(实例)参与,匹配的rules基本上是不变的,仅有两点:当一些函数等级一样时,若只有一个是非模板函数,那么此函数要优于其他模板函数;若都是模板函数,那么更specialized的模板函数要优于其他模板函数。下面的代码中,两个模板都是exact
match(第二个模板虽然需要将数组转为指针,但根据6.23,这种转换属于exact match),但第二个模板要更specialized,所以与第二个模板匹配。

template <typename T>
void f(const T&){
cout << "f(const T&)" << endl;
}
template <typename T>
void f(T*){
cout << "f(T*)" << endl;
}
f("abc");//call f(T*)


16.27 C++11引入了variadic template(变参模板)。通常不固定的参数被叫做parameter pack(代表0个或者更多参数),所以现在就有两种parameter pack:template parameter pack和function parameter pack。在模板参数列表中,class ...或者typename ...表示后面的参数是是一系列(0个或更多)类型;如果类型后面跟...,表示后面的参数是一系列(0个或更多)给定类型的nontype参数。注意,在函数模板的函数参数列表中,一些函数的类型可能是template
parameter pack。同普通模板函数一样,变参函数模板的参数可以由编译器推演,也可以显式指定。

// Args is a template parameter pack; rest is a function parameter pack
// Args represents zero or more template type parameters
// rest represents zero or more function parameters
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);

16.28 针对parameter pack,C++11引入了sizeof...操作符来获得pack中元素的个数。

template<typename ... Args> void g(Args ... args) {
cout << sizeof...(Args) << endl; // number of type parameters
cout << sizeof...(args) << endl; // number of function parameters
}

16.29 变参函数模板,通常是递归函数,那样就需要定义一个对应的非变参函数模板或非模板函数,并且需要expand parameter pack。在展开时,通常对pack设置了一个pattern,在pattern的后边跟上...,展开操作就是将该pack的元素分散开,并且对每个元素应用该pattern。如下面的const Args&...展开,就是将Args展开成多个单独的类型type,再应用pattern后,每个类型就是const tye&;同理,foo(rest)...展开就需要对每个元素调用函数foo。
// function to end the recursion and print the last element
// this function must be declared before the variadic version of print is defined
template<typename T>
ostream &print(ostream &os, const T &t){
return os << t; // no separator after the last element in the pack
}
// this version of print will be called for all but the last element in the pack
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest)// expand Args
{
os << t << ", ";
return print(os, rest...); // expand rest
}
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
// print(os, foo(a1), foo(a2), ..., foo(an)
return print(os, foo(rest)...);
}

16.30 在C++11标准中,可以将变参模板与forward配合使用。实际上,变参函数模板能够与任意的函数模板配合使用的。

template <class... Args>
void emplace_back(Args&&... args){
chk_n_alloc(); // reallocates the vector if necessary
alloc.construct(first_free++, std::forward<Args>(args)...);
}
//测试代码;
template <typename T1, typename T2>
T1 foo2(const T1& t1, const T2& t2){
return t1 * t2;
}
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest){
//return print(os, foo2(rest, 2)...);// also is ok!
return print(os, foo2<Args, int>(res, 2)...); //ok
}

16.31 函数模板的specialization(特化)的形式如下(以对函数模板template <typename T> int compare(const T&, const T&)定义针对const char*的特化为例)。注意,compare函数名后面是可以跟一对尖括号的,当每个参数都能推演出来时,此尖括号可以为空,否则需要显式指定(当然可以全部指定)。

// special version of compare to handle pointers to const character arrays
template <>
int compare(const char* const &p1, const char* const &p2){
return strcmp(p1, p2);
}

16.32 模板的特化实际上就是定义了一个特殊的实例化,即特化本质是实例化,所以特化对函数的重载(匹配)是没有任何影响的。像其他实例化一样,在特化之前,需要看到原模板的声明。另外,在使用(特化版本的)实例化之前,要看到特化的声明,否则,就是会根据原模板,自动定义一个实例化;如果,在程序中同时使用了同一个模板参数的特化版本和实例化版本,这种做法是错误的(虽然一般编译器都不会去检测)。通常将模板和特化声明在同一个文件中。另外i注意,一个版本的特化只能定义一次,不能重复定义,所以特化的定义通常放在源文件(*.cpp,*.cc文件)中(与16.3小项不同)。

16.33 特化时,通常(最好)与原模板定义在同一个命名空间下面,既是是std空间也可以(原则上std命名空间是不建议(不允许)向里面添加东西的,但是特化(不是偏特化)是可以的,也是提倡的)。所以为我们自己定义的类定义std::hash模板类的特化版本如下。由于类模板的特化后是类,所以类的特化应该定义在头文件中,但是特化后类的成员函数可以(若在类外定义,就是必须)定义在源文件(*.cpp,*.cc文件)中(与16.3小项不同)。

namespace std {
template <> // we're defining a specialization with
struct hash<Sales_data>{ // the template parameter of Sales_data
// the type used to hash an unordered container must define these types typedef size_t result_type;
typedef Sales_data argument_type; // by default, this type needs ==
size_t operator()(const Sales_data& s) const;
// our class uses synthesized copy control and default constructor
};
size_t hash<Sales_data>::operator()(const Sales_data& s) const {
return hash<string>()(s.bookNo) ^
hash<unsigned>()(s.units_sold) ^
hash<double>()(s.revenue);
}
} // close the std namespace; note: no semicolon after the close curly
//若按下面这种结构实现,也是添加在std命名空间
template <>
struct std::hash<Sales_data>{/**/};
size_t std::hash<Sales_data>::operator()(const Sales_data& s) const {/**/}

16.34 与函数模板不一样,类模板可以做partial specialization(偏特化),偏特化之后仍然是模板。以library中remove_reference模板的实现为例:

template <class T> struct remove_reference {
typedef T type;
};
template <class T> struct remove_reference<T&>{ // lvalue references
typedef T type;
};
template <class T> struct remove_reference<T&&>{ // rvalue references
typedef T type;
};
int i;
// decltype(42) is int, uses the original template
remove_reference<decltype(42)>::type a;
// decltype((i)) is int&, uses first (T&) partial specialization
// 在C++Primer P.711用的是decltype(i),个人认为是作者的真实意图是要用decltype((i))
remove_reference<decltype((i))>::type b;
// decltype(std::move(i)) is int&&, uses second (i.e., T&&) partial specialization
remove_reference<decltype(std::move(i))>::type c;

16.35 Specializing members but not the class。这应该也算是类的特化。

template <typename T> struct Foo {
Foo(const T &t = T()): mem(t) { }
void Bar() { /* ... */ }
T mem;
};
template<> // we're specializing a template
void Foo<int>::Bar(){ // we're specializing the Bar member of Foo<int>
// do whatever specialized processing that applies to ints
}

16.36 注意模板的(偏)特化与模板类型别名(见16.8 )的区别。别名,没有对模板的(部分)类型的实现进行重定义,这是与特化的本质区别。

注:本文以《C++ Primer(英文版)》(5th Edition)为参考。

总共由四部分组成:

《C++ Primer (5th Edition)》笔记-Part I. The Basics

《C++ Primer (5th Edition)》笔记-Part II. The C++ Library

《C++ Primer (5th Edition)》笔记-Part III. Tools For Class Authors

《C++ Primer (5th Edition)》笔记-Part IV. Advanced Topics
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: