Effective C++ 中文版(第三版)读书笔记 更新ing~

Effective C++ 中文版(第三版)持续更新ing

  • 让自己习惯C++
    • 条款1 视c++为一个联邦
    • 条款2 尽量以const,enum,inline替换#define
    • 条款3 尽可能使用const
    • 条款4 确定对象使用前已被初始化
  • 构造/析构/赋值/运算
    • 条款5 了解C++默默编写和调用哪些函数
    • 条款6 若不想使用编译器自动生成的函数,应该想办法拒绝
    • 条款7 为多态基类声明virtual析构函数
    • 条款8 别让异常逃离析构函数(Prevent exceptions from leaving destructors)
    • 条款9 绝不在构造和析构过程中调用virtual函数
    • 条款10 令operator=返回一个reference to *this
    • 条款11 令operator=中处理自我赋值
    • 条款12 复制对象时勿忘其每一个成分
    • 条款13 以对象管理资源
    • 条款14 在资源管理类中小心copying行为
    • 条款15 在资源管理类中提供对原始资源的访问
    • 条款16 成对使用new和delete时要采用相同形式
    • 条款17 以独立语句将newed对象置入智能指针
    • 条款18让接口容易被正确使用,不易被误用
    • 条款19 设计class犹如设计type
    • 宁以pass-by-reference-to-const替换pass-by-value
    • 条款21 必须返回对象时,别妄想返回其reference
    • 条款22. 将成员变量声明为private
    • 条款23 宁以non-member、non-friend替换member函数
    • 条款24 若所有参数皆需类型转换,请为此采用non-member函数
    • 条款25 考虑写出一个不抛异常的swap函数
  • 实现
    • 条款26 尽可能延后变量定义式的出现时间
    • 条款27 尽量少做转型动作
    • 条款28 避免返回handles指向对象内部成分
    • 条款29 为”异常安全”而努力是值得的
    • 条款30透彻了解inlining的里里外外(Understand the ins and outs of inlining)
    • 条款31 将文件间的编译依存关系降至最低(Minimize compilation dependencies between files)
    • 条款32 确定你的public继承塑模出is-a关系(Make sure public inheritance models “is-a”)
    • 条款33 避免遮挡继承而来的名称(Avoid hiding inherited names)
    • 条款34 区分接口继承和实现继承(Differentiate between inheritance of interface and inheritance of implementation)
    • 条款34 考虑virtual函数以外的其它选择(Consider alternatives to virtual functions)
    • 条款34 绝不重新定义继承而来的non-virtual函数(Never redefine an inherited non-virtual function)
    • 条款37 绝不重新定义继承而来的缺省参数值(Never redefine a function’s inherited default parameter value)
    • 条款38 通过复合塑模出has-a或”根据某物实现出”(Model “has-a” or “is-implemented-in-terms-of” through composition)
    • 条款39 明智而审慎地使用private继承(Use private inheritance judiciously)
    • 条款40 明智而审慎地使用多重继承(Use multiple inheritance judiciously)
    • 条款41 了解隐式接口和编译期多态(Understand implicit interfaces and compile-time polymorphism)
    • 条款42 了解typename的双重意义(Understand the two meanings of typename)
    • 条款43 学习处理模板化基类内的名称(Know how to access names in templatized base classes)

2022.1.20 终于看完了,由于忙于学业,断断续续的才读完,简单来说这本书让我受益良多,值得好好总结,给我这个小白打开Cpp的另一个维度,还有很多等待我去发现,挺有意思的哈哈,内容更新这几天完成~

让自己习惯C++

  • 类中声明构造函数时,隐式类型转换非必要的时候,最好在构造函数开头加 explict,加了之后也是允许显式类型转换 。
class B
{ 
Explict B(int x=0;bool b=ture); 
};

doSomething(B(10))  //发生显式类型转换,B的构造函数将int 显式转换

条款1 视c++为一个联邦

c语言的四个次语言:

  1. C
  2. Object—Oriented C++ :构造函数析构函数、封装、继承、多态、函数
  3. Template C++:泛型编程
  4. STL:template 程序库 ——容器、迭代器、算法、函数对象

条款2 尽量以const,enum,inline替换#define

  1. 对于单纯常量,最好以const对象或enums替换#defines

  2. 对于形似函数的宏(macros),最好改用inline函数替换#defines

#define  CALL_MAX(a,b) func((a)>(b)?(a):(b))
     
 //改为
  template
           Inline void callMax(const T&a,const T&b)
{
  Func((a)>(b)?(a):(b));
}

条款3 尽可能使用const

  • const出现在 char* 左边 表示被指物事常量,出现在char*右边表示指针自身是常量

const char* const p =

  • const_iterator 可将stl模拟一个 const T* 指针。即被指物不可改变。
  Vectorconst_iterator it=
  • 开头加const 的迭代器 即 迭代器本身不可改变
const Vectoriterator it= 
  • 不需要改动的值最好加const 省去后续麻烦

  • 静态成员函数 不可访问 非静态成员变量,除非 在静态成员变量的数据类型前 加mutable。加完之后静态成员函数 可以对非静态成员进行操作

const_cast   constexpression  
//表示将const 转除,即 const 转为 non-const

注:new_type为目标数据类型,expression为原始数据类型变量或者表达式。

  • const 函数不能设法去调用non—const函数,反过来调用 会避免代码重复

条款4 确定对象使用前已被初始化

  • 为内置型对象(int、double等)进行手工初始化
// 初始化a
 int a=0//初始化b
 double b;
 std::cin>>b;
  • 构造函数最好使用成员初值列 对所有成员变量进行初始化(最好不要遗漏掉,需要赋初值的成员变量,如deptWork_count)
class Teacher
{
public:
Teacher(const string name,const int age):m_name(name),m_age(age),deptWork_count(0)
{ }
string m_name;
int m_age;
int deptWork_count;
};
  • 用local-static 对象 替换 non-local-static 对象。(这是基于c++保证,函数内的local-static 对象会在“该函数被调用期间”“首次遇上该对象之定义式子”时被初始化,不会出现不同编译单元初始化次序的纠结)
class Teacher{  ·····};
Teacher& T()
{
static Teacher t;
return t//返回指向“static对象”的reference,而不是自身。
}

构造/析构/赋值/运算

条款5 了解C++默默编写和调用哪些函数

编译器可以暗自为class创建default构造函数、copy assignment 操作符(“ = ”操作符重载)、析构函数

条款6 若不想使用编译器自动生成的函数,应该想办法拒绝

  1. 两种方法:
  • 可将相应的成员函数声明为private并且不予以实现
class Home{
  public/.../
  private: //只声明不实现 ,拷贝构造函数与操作符重载
  Home(const Home&;
  Home operator=(const Home&);
};
  • 使用基类这一方法,让子类继承达到防止拷贝的效果
class Home{
  public:
  Home(){};
  ~Home(){};
  private: // ,拷贝构造函数与操作符重载
  Home(const Home&;
  Home operator=(const Home&);
};

class HomeTVprivate Home { //TV类不再声明防拷贝相关函数。
  /.../
};

条款7 为多态基类声明virtual析构函数

  1. 带有多态性质的base class 应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该有一个virtual析构函数。 否则会造成“局部析构”,如子类析构未调用。发生资源泄露、数据结构破坏、浪费调试时间。
  2. class的设计目的如果不是为了作为base class,或不需要具备多态性,就不该声明virtual析构函数。
  3. vptrvtbl
    • 欲实现virtual函数,对象需要携带某些信息,该信息由vptr指出。
    • vptr(virtual table pointer)是一个指针指向一个由函数指针构成的数组,该数组成为vtbl(virtual table)。
    • 每个带有virtual函数(一个或多个)的class都有一个相应的vtbl。

条款8 别让异常逃离析构函数(Prevent exceptions from leaving destructors)

C++并不禁止析构函数吐出异常,但它不鼓励你这样做。

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。(如下图)

条款9 绝不在构造和析构过程中调用virtual函数

  • derived class对象内的base class成分会在derived class自身成分被构造之前先构造妥当。由于base class构造函数的执行更早于derived class构造函数(构造先有父后有子,析构则没子再没父),当base class构造函数执行时derived class的成员变量尚未初始化。确定你的构造函数和析构函数都没有(在对象被创建和被销毁期间)调用virtual函数,而它们调用的所有函数也都服从同一约束。
  • 利用辅助函数创建一个值给base class更好,具有可读性。令此函数为static也保证该信息不会在未初始化状态下被调用。

  1. 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。可以改为由derived class上传参数至base class

条款10 令operator=返回一个reference to *this

返回左侧对象


class Widget {
public:
	Widget& operator= (const Widget& rhs) // 返回类型是个reference,指向当前对象
	{
		return *this;
	}
 
	Widget& operator+= (const Widget& rhs) // 这个协议适用于+=、-=、*=等等
	{
		return *this;
	}
 
	Widget& operator= (int rhs) // 此函数也适用,即使此一操作符的参数类型不符协定
	{
		return *this;
	}

条款11 令operator=中处理自我赋值

  1. 在operator=中处理”自我赋值”(Handle assignment to self in operator=)
class Widget11 {
public:
	void swap(Widget11& rhs) {} // 交换*this和rhs的数据
 
	Widget11& operator= (const Widget11& rhs)//可将& 去掉,函数内同时不实现交换操作,
	                                        //在构造过程中调用拷贝构造进行,这样会使得程序高效,
	                                        //但牺牲了清晰性
	{
		Widget11 temp(rhs); // 为rhs数据制作一份复件(副本)
		swap(temp); // 将*this数据和上述复件的数据交换
		return *this;
	}
};
 
int test_item_11() //要避免这样! 采用上述swap代替赋值
{
	Widget11 w;
	w = w; // 赋值给自己
 
	return 0;
}

在operator=函数内手工排列语句(确保代码不但”异常安全”而且”自我赋值安全”)的一个替代方案是,使用所谓的copy and swap技术。

请记住:

  • 确保当对象自我赋值时operator=有良好行为。其中技术包括比较”来源对象”和”目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

条款12 复制对象时勿忘其每一个成分

void logCall(const std::string& funcName) {} // 制造一个log entry
 
class Customer {
public:
	Customer(const Customer& rhs)
		: name(rhs.name) // 复制rhs的数据
	{
		logCall("Customer copy constructor");
	}
 
	Customer& operator= (const Customer& rhs)
	{
		logCall("Customer copy assignment operator");
		name = rhs.name; // 复制rhs的数据
		return *this;
	}
 
private:
	std::string name;
};
 
class PriorityCustomer : public Customer {
public:
	PriorityCustomer(const PriorityCustomer& rhs)
		: Customer(rhs), // 调用base class的copy构造函数
		  priority(rhs.priority)
	{
		logCall("PriorityCustormer copy constructor");
	}
 
	PriorityCustomer& operator= (const PriorityCustomer& rhs)
	{
		logCall("PriorityCustomer copy assignment operator");
		Customer::operator=(rhs); // 对base class成分进行赋值操作
		priority =  rhs.priority;
		return*this;
	}
 
private:
	int priority;
};
  • 如果你为class添加一个成员变量,你必须同时修改copying函数(包括copy构造函数和copy assignment操作符)。(你也需要修改class的所有构造函数以及任何非标准形式的operator=。如果你忘记,编译器不太可能提醒你。)

  • 任何时候只要你承担起”为derived class撰写copying函数”的重责大任,必须很小心地也复制其base class成分。那些成分往往是private,所以你无法直接访问它们,你应该让derived class的copying函数调用相应的base class函数。

  • 令copy assignment操作符调用copy构造函数是不合理的,因为这就像试图构造一个已经存在的对象。令copy构造函数调用copy assignment操作符同样无意义。构造函数用来初始化新对象,而assignment操作符只施行于已初始化对象身上。

  • 如果你发现你的copy构造函数和copy assignment操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是private而且常被命名为init。这个策略可以安全消除copy构造函数和copy assignment操作符之间的代码重复。

条款13 以对象管理资源

  • 把资源放进对象内,我们便可依赖C++的”析构函数自动调用机制”确保资源被释放。

  • auto_ptr是个”类指针(pointer-like)对象”,也就是所谓的”智能指针”,其析构函数自动对其所指对象调用delete。一定要注意别让多个auto_ptr同时指向同一对象,如果真是那样,对象会被删除一次以上。注:在C++11中auto_ptr已经被废弃,用unique_ptr替代。

  • std::unique_ptr用法参考:https://blog.csdn.net/fengbingchun/article/details/52203664

  • auto_ptr的替代方案是”引用计数型智慧指针”(reference-counting smart pointer, RCSP)。所谓RCSP也是个智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。类似于C++11中的shared_ptr。

  • std::shared_ptr用法参考:https://blog.csdn.net/fengbingchun/article/details/52202007

请记住

  1. 为防止资源泄漏,请使用RAII(Resource Acquisition Is Initialization,资源取得时机便是初始化时机)对象,它们在构造函数中获得资源并在析构函数中释放资源。
  2. 两个常被使用的RAII classes分别是stdn::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。

条款14 在资源管理类中小心copying行为

  • Copying函数(包括copy构造函数和copy assignment操作符)有可能被编译器自动创建出来,因此除非编译器所生版本做了你想要做的事,否则你得自己编写它们。

请记住:

  1. .复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
  2. 普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法(reference counting)。不过其它行为(复制底部资源、转移底部资源控制权)也都可能被实现。

条款15 在资源管理类中提供对原始资源的访问

请记住:

  1. APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个”取得其所管理之资源”的办法。
  2. 对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。

条款16 成对使用new和delete时要采用相同形式

int test_item_16()
{
	std::string* stringPtr1 = new std::string;
	std::string* stringPtr2 = new std::string[100];
 
	delete stringPtr1;    // 删除一个对象
	delete [] stringPtr2; // 删除一个由对象组成的数组
 
	typedef std::string AddressLines[4]; // 每个人的地址有4行,每行是一个string
	std::string* pal = new AddressLines; // 注意:"new AddressLines"返回一个string*,就像"new string[4]"一样
 
	//delete pal;    // 行为未有定义
	delete [] pal; // 很好
 
	return 0;
}

当你使用new(也就是通过new动态生成一个对象),有两件事发生:第一,内存被分配出来(通过名为operator new的函数);第二,针对此内存会有一个(或更多)构造函数被调用。当你使用delete,也有两件事发生:针对此内存会有一个(或更多)析构函数被调用,然后内存才被释放(通过名为operator delete的函数)。

当你对着一个指针使用delete,唯一能够让delete知道内存中是否存在一个”数组大小记录”的办法就是:由你来告诉它。如果你使用delete时加上中括号(方括号),delete便认定指针指向一个数组,否则它便认定指针指向单一对象。

如果你调用new时使用[],你必须在对应调用delete时也使用[]。如果你调用new时没有使用[],那么也不该在对应调用delete时使用[]。

最好尽量不要对数组形式做typedef动作。因为C++标准程序库含有string、vector等template,可将数组的需求降至几乎为零。

  • 请记住:如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。

条款17 以独立语句将newed对象置入智能指针

class Widget17 {};
int priority() { return 0; }
void processWidget(std::shared_ptr<Widget17> pw, int priority) {}
 
int test_item_17()
{
	// 执行new Widget17; 调用priority; 调用std::shared_ptr构造函数,它们的执行顺序不确定
	processWidget(std::shared_ptr<Widget17>(new Widget17), priority()); // 可能泄露资源
	
	std::shared_ptr<Widget17> pw(new Widget17); // 在单独语句内以智能指针存储newed所得对象
	processWidget(pw, priority()); // 这个调用动作绝不至于造成泄露
 
	return 0;
}
  • 请记住:以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。

条款18让接口容易被正确使用,不易被误用

  • 请记住:(1).好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。(2).”促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。(3).”阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。(4).std::shared_ptr支持定制型删除器(custom deleter)。这可防范DLL问题,可被用来自动解除互斥锁(mutex)等等。

条款19 设计class犹如设计type

宁以pass-by-reference-to-const替换pass-by-value

如果你有个对象属于内置类型(例如int),pass by value往往比pass by reference的效率高些。

  • 请记住:(1).尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem)。(2).以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。

条款21 必须返回对象时,别妄想返回其reference

  • 推荐
class Rational {
public:
	Rational(int numerator = 0, int denominator = 1) : n(numerator), d(denominator)
	{}
 
private:
	int n, d; // 分子(numerator)和分母(denominator)
 
// 返回const Rational可以预防"没意思的赋值动作": Rational a, b, c; (a * b) = c;
friend const Rational operator* (const Rational& lhs, const Rational& rhs)
{
	return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
};
  • 反例
friend const Rational operator* (const Rational& lhs, const Rational& rhs)
{
	Rational result(lhs.n * rhs.n, lhs.d * rhs.d);//警告!!!糟糕的代码
	return  result
}

所谓reference只是个名称,代表某个既有对象。任何时候看到一个reference声明式,你都应该立刻问自己,它的另一个名称是什么?因为它一定是某物的另一个名称。任何函数如果返回一个reference指向某个local对象,都将一败涂地。(如果函数返回指针指向一个local对象,也是一样。)

  • 请记住:绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。

条款22. 将成员变量声明为private

  • 请记住:(1).切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。(2).protected并不比public更具封装性。

条款23 宁以non-member、non-friend替换member函数

请记住:宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。

条款24 若所有参数皆需类型转换,请为此采用non-member函数

class Rational24 {
public:
	Rational24(int numerator = 0, int denominator = 1) {} // 构造函数刻意不为explicit,允许int-to-Rational24隐式转换
	int numerator() const { return 1; } // 分子(numerator)的访问函数
	int denominator() const { return 2; } // 分母(denominator)的访问函数
 
	/*const Rational24 operator* (const Rational24& rhs) const
	{
		return Rational24(this->n * rhs.numerator(), this->d * rhs.denominator());
	}*/
 
private:
	int n, d;
};
 
const Rational24 operator* (const Rational24& lhs, const Rational24& rhs) // non-member函数
{
	return Rational24(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
 
int test_item_24()
{
	Rational24 oneEighth(1, 8);
	Rational24 oneHalf(1, 2);
	Rational24 result = oneHalf * oneEighth; // 很好
	result = result * oneEighth; // 很好
 
	result = oneHalf * 2; // 很好,隐式类型转换(implicit type conversion)
	result = 2 * oneHalf; // 错误, only non-member function success
 
	// 以对应的函数形式重写上述两个式子
	//result = oneHalf.operator*(2); // 很好, only member function success
	//result = 2.operator*(oneHalf); // 错误, only non-member function success
 
	result = operator*(2, oneHalf); // 错误, only non-member function success
 
	return 0;
}

无论何时如果你可以避免friend函数就该避免。

  • 请记住:如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。

条款25 考虑写出一个不抛异常的swap函数

class WidgetImpl { // 针对Widget25数据而设计的class
public:
private:
	int a, b, c; // 可能有许多数据,意味复制时间很长
	std::vector<double> v;
};
 
class Widget25 { // 这个class使用pimpl(pointer to implementation)手法
public:
	Widget25(const Widget25& rhs) {}
	Widget25& operator= (const Widget25& rhs) // 复制Widget25时,令它复制其WidgetImpl对象
	{
		*pImpl = *(rhs.pImpl);
 
		return *this;
	}
 
	void swap(Widget25& other)
	{
		using std::swap;
		swap(pImpl, other.pImpl); // 若要置换Widget25就置换其pImpl指针
	}
 
private:
	WidgetImpl* pImpl; // 指针,所指对象内含Widget25数据
};
 
// std::swap针对Widget25特化版本
namespace std {
template<>
void swap<effective_cplusplus_::Widget25>(effective_cplusplus_::Widget25& a, effective_cplusplus_::Widget25& b)
{
	a.swap(b); // 若要置换Widget25,调用其swap成员函数
}
 
}

所谓swap(置换)两对象值,意思是将两对象的值彼此赋予对方。缺省情况下swap动作可由标准程序库提供的swap算法完成。

一般而言,重载function template没有问题,但std是个特殊的命名空间,其管理规则也比较特殊。客户可以全特化std内的template,但不可以添加新的template(或class或function或其它任何东西)到std里头。

首先,如果swap的缺省实现码对你的class或class template提供可接受的效率,你不需要额外做任何事。其次,如果swap缺省实现版的效率不足,试着做以下事情
(1). 提供一个public swap成员函数,让它高效地置换你的类型的两个对象值。这个函数不该抛出异常。
(2). 在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。
Effective C++ 中文版(第三版)读书笔记 更新ing~_第1张图片

(3). 如果你正在编写一个class(而非class template),为你的class特化std::swap。并令它调用你的swap成员函数。 最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap。
Effective C++ 中文版(第三版)读书笔记 更新ing~_第2张图片

  • 请记住:(1).当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。(2).如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非template),也请特化std::swap。(3).调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何”命名空间资格修饰”。(4).为”用户定义类型”进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。

实现

条款26 尽可能延后变量定义式的出现时间

  • 变量定义在循环内还是循环外的抉择
  1. 方法A:一次构造 + 一次析构+n次赋值
  2. 方法B: n 次构造 + n 次析构

如果一此赋值操作成本 低于 一次构造 + 析构的成本,则采用A 否在采用B。但是A做法由于一次构造作用域覆盖整个循环,对于后续代码可读性和维护性造成影响。一般采用B。

不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。如果这样,不仅能够避免构造(和析构)非必要对象,还可以避免无意义的default构造行为。另外能够提高程序效率和代码清晰度。

  • 请记住:尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。

条款27 尽量少做转型动作

C++引入4个新型转型操作符的详细介绍.
const_cast通常被用来将对象的常量性移除(cast away the constness)。它也是唯一有此能力的C+±style转型操作符。

dynamic_cast主要用来执行”安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。

reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为一个int。

static_cast用来强迫隐式转换(implicit conversions),例如将non-const对象转为const对象,或将int转为double等等。它也可以用来执行上述多种转换的反向转换,例如将void*指针转为typed指针,将pointer-to-base转为pointer-to-derived。但它无法将const转为non-const,这个只有const_cast才办得到。

  • 请记住:
    (1).如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无须转型的替代设计。
    (2).如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
    (3). 宁可使用C+±style(新式)转型,不要使用旧式转型(C风格转型)。前者很容易辨识出来,而且也比较有着分门别类的职掌。

条款28 避免返回handles指向对象内部成分

class Point { // 这个class用来表述"点"
public:
	Point(int x, int y) {}
	void setX(int newVal) {}
	void setY(int newVal) {}
};
 
struct RectData { // 这些"点"数据用来表现一个矩形
	Point ulhc; // ulhc = "upper left-hand corner"(左上角)
	Point lrhc; // lrhc = "lower right-hand corner"(右上角)
};
 
class Rectangle {
public:
	Rectangle(const Point&, const Point&) {}
	Point& upperLeft() const { return pData->ulhc; } //不允许!!这样返回一个内部指针,用户可以对某些数据进行修改
	Point& lowerRight() const { return pData->lrhc; } //不允许!!这样返回一个内部指针,用户可以对某些数据进行修改
 	
  	const Point& upperLeft() const { return pData->ulhc; } //允许!!用户不可更改 Point 类型指针指向的内容
	const Point& lowerRight() const { return pData->lrhc; }//允许!!用户不可更改 Point 类型指针指向的内容
	// 有了这样的改变,客户可以读取矩形的Point,但不能涂写它们
	// 但即使如此,也可能导致dangling handles(空悬的号码牌):这种handles所指东西(的所属对象)不复存在

 
private:
	std::shared_ptr<RectData> pData;
};
 
int test_item_28()
{
	Point coord1(0, 0);
	Point coord2(100, 100);
	const Rectangle rec(coord1, coord2); // rec是个const矩形,从(0,0)到(100,100)
 
	// upperLeft的调用者能够使用被返回的reference(指向rec内部的Point成员变量)来更改成员,但rec其实应该是不可变的(const)
	rec.upperLeft().setX(50); // 现在rec却变成从(50,0)到(100,100)
 
	return 0;
}

条款29 为”异常安全”而努力是值得的

“异常安全”有两个条件:(1).不泄漏任何资源。(2).不允许数据败坏。

异常安全函数(Exception-safe functions)提供以下三个保证之一:

(1).基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态(例如所有的class约束条件都继续获得满足)。然而程序的现实状态(exact state)恐怕不可预料。

(2).强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功;如果函数失败,程序会回复到”调用函数之前”的状态。

(3).不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(例如int,指针等等)身上的所有操作都提供nothrow保证。这是异常安全码中一个必不可少的关键基础材料。

异常安全码(Exception-safe code)必须提供上述三种保证之一。如果它不这样做,它就不具备异常安全性。

有个一般化的设计策略很典型地会导致强烈保证,这个策略被称为copy and swap。原则很简单:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。但一般而言它并不保证整个函数有强烈的异常安全性。(因为当函数中也有调用其他函数,不能保证调用时不发生异常)

  • 请记住:
    (1).异常安全函数(Exception-safe function)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
    (2).”强烈保证”往往能够以copy-and-swap实现出来,但”强烈保证”并非对所有函数都可实现或具备现实意义。
    (3).函数提供的”异常安全保证”通常最高只等于其所调用之各个函数的”异常安全保证”中的最弱者。

条款30透彻了解inlining的里里外外(Understand the ins and outs of inlining)


inline void f() {} // 假设编译器有意愿inline“对f的调用”
 
int test_item_30()
{
	void (*pf)() = f; // pf指向f
 
	f(); // 这个调用将被inlined,因为它是一个正常调用
	pf(); // 这个调用或许不被inlined,因为它通过函数指针达成
 
	return 0;
}

inline函数背后的整体观念是,将”对此函数的每一个调用”都以函数本体替换之。

过度热衷inlining会造成程序体积太大(对可用空间而言)。即使拥有虚内存,inline造成的代码膨胀亦会导致额外的换页行为(paging),降低指令高速缓存装置的击中率(instruction cache hit rate),以及伴随这些而来的效率损失。

inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内。这样的函数通常是成员函数。friend函数如果被定义于class内,它们也是被隐喻声明为inline。明确声明inline函数的做法则是在其定义式前加上关键字inline。

inline函数通常一定被置于头文件内,因为大多数建置环境(build environments)在编译过程中进行inlining,而为了将一个”函数调用”替换为”被调用函数的本体”,编译器必须知道那个函数长什么样子。

inlining在大多数C++程序中是编译期行为。

templates通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子。(这其实也不是世界一统的准则。某些建置环境可以在链接期才执行template具现化。只不过编译期完成具现化动作比较常见。)template的具现化与inlining无关。

大部分编译器拒绝将太过复杂(例如带有循环或递归)的函数inlining,而所有对virtual函数的调用(除非是最平淡无奇的)也都会使inlining落空。

一个表面上看似inline的函数是否真是inline,取决于你的建置环境,主要取决于编译器。大多数编译器提供了一个诊断级别:如果它们无法将你要求的函数inline化,会给你一个警告信息。为数个inling函数声明entshit rate

编译器通常不对”通过函数指针而进行的调用”实施inlining,这意味对inline函数的调用有可能被inlined,也可能不被inlined,取决于该调用的实施方式。

实际上构造函数和析构函数往往是inlining的糟糕候选人。

inline函数无法随着程序库的升级而升级。换句话说如果f是程序库内的一个inline函数,客户将”f函数本体”编进其程序中,一旦程序库设计者决定改变f,所有用到f的客户端程序都必须重新编译。

一开始先不要将任何函数声明为inline,或至少将inlining施行范围局限在那些”一定成为inline”或”十分平淡无奇”的函数身上。慎重使用inline便是对日后使用调试器带来帮助。

  • 请记住:
    (1).将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
    (2).不要只因为function templates出现在头文件,就将它们声明为inline。(除非其templates具现化的每一个函数都为inline则可声明为inline。

条款31 将文件间的编译依存关系降至最低(Minimize compilation dependencies between files)

标准程序库组件不该被前置声明。

  • 设计策略:

(1). 如果使用object references或object pointers可以完成任务,就不要使用objects。你可以只靠一个类型声明式就定义出指向该类型的references和pointers;但如果定义某类型的objects,就需要用到该类型的定义式。

(2). 如果能够,尽量以class声明式替换class定义式。注意,当你声明一个函数而它用到某个class时,你并不需要该class的定义;纵使函数以by value方式传递该类型的参数(或返回值)亦然。

(3). 为声明式和定义式提供不同的头文件。为了促进严守上述准则,需要两个头文件,一个用于声明式,一个用于定义式。当然,这些文件必须保持一致性,如果有个声明式被改变了,两个文件都得改变。

  • Handle classes(使用pimpl idiom(pimpl是”pointer to implementation”的缩写))和Interface classes(特殊的abstract base class(抽象基类))解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性(compilation dependencies)。

  • Handle classes
    在Handle classes身上,成员函数必须通过implementation pointer取得对象数据。那会为每一次访问增加一层间接性。而每一个对象消耗的内存数量必须增加implementation pointer的大小。最后,implementation pointer必须初始化(在Handle class构造函数内),指向一个动态分配得来的implementation object,所以你将蒙受因动态内存分配(及其后的释放动作)而来的额外开销,以及遭遇bad_alloc异常(内存不足)的可能性。

  • interface classes
    至于interface classes,由于每个函数都是virtual,所以你必须为每次函数调用付出一个间接跳跃(indirect jump)成本。此外interface class派生的对象必须内含一个vptr(virtual table pointer),这个指针可能会增加存放对象所需的内存数量----实际取决于这个对象除了interface class之外是否还有其它virtual函数来源。

最后,不论handle classes或interface classes,一旦脱离inline函数都无法有太大作为。函数本体为了被inlined必须(很典型地)置于头文件内,但handle classes和interface classes正是被设计用来隐藏实现细节如函数本体。

然而,如果只因为若干额外成本便不考虑handle classes和interface classes,将是严重的错误

  • 请记住:
    (1). 支持”编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是handle classesinterface classes
    (2). 程序库头文件应该以”完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。(例如机房预约管理系统

条款32 确定你的public继承塑模出is-a关系(Make sure public inheritance models “is-a”)

  • public inheritance(公开继承)意味”is-a”(是一种)的关系。

  • 如果你令class D(“Derived”)以public形式继承class B(“Base”),你便是告诉C++编译器(以及你的代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。

请记住:

  1. public继承”意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。

条款33 避免遮挡继承而来的名称(Avoid hiding inherited names)

class Base {
private:
	int x;
 
public:
	virtual void mf1() = 0;
	virtual void mf1(int) {}
	virtual void mf2() {}
	void mf3() {}
	void mf3(double) {}
};
 
class Derived : public Base {
public:
	virtual void mf1() {}
	void mf3() {}
	void mf4() {}
};
 
class Derived33 : public Base {
public:
	// 必须为那些原本会被遮掩的每个名称引入一个using声明式,否则某些你希望继承的名称会被遮掩
	using Base::mf1; // 让Base class内名为mf1和mf3的所有东西在Derived作用域内都可见(并且public)
	using Base::mf3;
	virtual void mf1() {}
	void mf3() {}
	void mf4() {}
};
 
int test_item_33()
{
	Derived d;
	int x = 0;
 
	d.mf1(); // 没问题,调用Derived::mf1
	//d.mf1(x); // 错误,因为Derived::mf1遮掩了Base::mf1
	d.mf2(); // 没问题,调用Base::mf2
	d.mf3(); // 没问题,调用Derived::mf3
	// Derived内的函数mf3遮掩了一个名为mf3但类型不同的Base函数
	//d.mf3(x); // 错误,因为Derived::mf3遮掩了Base::mf3
 
	Derived33 d2;
 
	d2.mf1(); // 仍然没问题,仍然调用Derived::mf1
	d2.mf1(x); // 现在没问题了,调用Base::mf1
	d2.mf2(); // 仍然没问题,调用Base::mf2
	d2.mf3(); // 没问题,调用Derived::mf3
	d2.mf3(x); // 现在没问题了,调用Base::mf3
 
	return 0;
}

请记住
(1). derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
(2).为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding functions)。

条款34 区分接口继承和实现继承(Differentiate between inheritance of interface and inheritance of implementation)

  • 成员函数的接口总是会被继承。

  • 声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。

  • 声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。

  • 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。

请记住
(1).接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
(2). pure virtual函数只具体指定接口继承。
(3). 简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。(4). non-virtual函数具体指定接口继承以及强制性实现继承。

条款34 考虑virtual函数以外的其它选择(Consider alternatives to virtual functions)

NVI手法实现Template Method模式
class GameCharacter {
public:
	int healthValue() const // derived classes不重新定义它
	{
		// ... // 做一些事前工作
		int retVal = doHealthValue(); // 做真正的工作
		// ... // 做一些事后工作
 
		return retVal;
	}
 
private:
	virtual int doHealthValue() const // derived classes可重新定义它
	{
		return 10;
	}
};
 
/*********************************************************************************/

// Strategy设计模式的简单应用
class GameCharacter35; // 前置声明(forward declaration)
int defaultHealthCalc(const GameCharacter35& gc) { return 1; }
class GameCharacter35 {
public:
	typedef int (*HealthCalcFunc)(const GameCharacter35&);
	//(HealthCalcFunc hcf = defaultHealthCalc)不给定实参也能调用,或者给定一个默认参数hcf,两者产生的对象相同
	explicit GameCharacter35(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {}
	int healthValue() const
	{
		return healthFunc(*this); 
	}
 
private:
	HealthCalcFunc healthFunc;
};
 
class EvilBadGuy : public GameCharacter35 {
public:
	explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter35(hcf) {}
};
 
int loseHealthQuickly(const GameCharacter35&) { return 2; } // 健康指数计算函数1
int loseHealthSlowly(const GameCharacter35&) { return 3; } // 健康指数计算函数2
 
/*********************************************************************************/

// 藉由std::function完成Strategy模式
class GameCharacter35_1;
int defaultHealthCalc_1(const GameCharacter35_1& gc) { return 11; }
class GameCharacter35_1 {
public:
	// HealthCalcFunc可以是任何"可调用物"(callae entity),可被调用并接受
	// 任何兼容于GameCharacter35_1之物,返回任何兼容于int的东西
	typedef std::function<int (const GameCharacter35_1&)> HealthCalcFunc;
	explicit GameCharacter35_1(HealthCalcFunc hcf = defaultHealthCalc_1) : healthFunc(hcf) {}
	int healthValue() const
	{
		return healthFunc(*this);
	}
 
private:
	HealthCalcFunc healthFunc;
};
 
int test_item_35()
{
	EvilBadGuy ebg1(loseHealthQuickly); // 相同类型的人物搭配不同的健康计算方式
	EvilBadGuy ebg2(loseHealthSlowly);
 
	return 0;
}
  • 藉由Non-Virtual Interface手法实现Template Method模式:这一基本设计,也就是”令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。它是所谓Template Method设计模式(与C++ templates并无关联)的一个独特表现形式。把这个non-virtual函数称为virtual函数的外覆器(wrapper)。在NVI手法下其实没有必要让virtual函数一定得是private。

  • Template Method模式介绍参考:https://blog.csdn.net/fengbingchun/article/details/30990475

  • 藉由Function Pointers实现Strategy模式。

Strategy模式介绍参考:https://blog.csdn.net/fengbingchun/article/details/32179281

  • 藉由std::function完成Strategy模式。

std::function介绍参考:https://blog.csdn.net/fengbingchun/article/details/52562918

  • 当你为解决问题而寻找某个设计方法时,不妨考虑virtual函数的替代方案:它们各自有其相对的优点和缺点

    (1). 使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性(private或protected)的virtual函数。

    (2). 将virtual函数替换为”函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。

    (3). 以std::function成员变量替换virtual函数,因而允许使用任何可调用物(callable entity)搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。

    (4). 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现方法。

请记住
(1). virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
(2). 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
(3). std::function对象的行为就像一般函数指针。这样的对象可接纳”与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)。

条款34 绝不重新定义继承而来的non-virtual函数(Never redefine an inherited non-virtual function)

class B {
public:
	void mf() { fprintf(stdout, "B::mf\n"); }
};
 
class D : public B {
public:
	void mf() { fprintf(stdout, "D::mf\n"); } // 遮掩(hides)了B::mf
};
 
int test_item_36()
{
	D x;
	B* pB = &x;
	// 由于pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本,即使pB指向一个类型为"B派生之class"的对象
	pB->mf(); // 调用B::mf
 
	D* pD = &x;
	pD->mf(); // 调用D::mf
 
	return 0;
}
  • non-virtual函数都是静态绑定(statically bound),virtual函数确是动态绑定(dynamically bound)。

请记住:绝对不要重新定义继承而来的non-virtual函数。

条款37 绝不重新定义继承而来的缺省参数值(Never redefine a function’s inherited default parameter value)

class Shape37 {
public:
	enum ShapeColor { Red, Green, Blud };
	// 所有形状都必须提供一个函数,用来绘出自己
	virtual void draw(ShapeColor color = Red) const = 0;
};
 
class Rectangle37 : public Shape37 {
public:
	// 注意,赋予不同的缺省参数值。这真糟糕!
	virtual void draw(ShapeColor color = Green) const
	{
		fprintf(stdout, "rectangle shape color: %d\n", color);
	}
};
 
class Circle37 : public Shape37 {
public:
	virtual void draw(ShapeColor color) const
	// 请注意,以上这么写则当客户以对象调用此函数,一定要指定参数值。因为静态绑定下这个函数并
	// 不从其base继承缺省参数值。但若以指针(或reference)调用此函数,可以不指定参数值,因为动态
	// 绑定下这个函数会从其base继承缺省参数值
	{
		fprintf(stdout, "circle shape color: %d\n", color);
	}
};
 
int test_item_37()
{
	Shape37* ps; // 静态类型为Shape37*;没有动态类型,因为它尚未指向任何对象
	Shape37* pc = new Circle37; // 静态类型为Shape37*,动态类型为Circle37*
	Shape37* pr = new Rectangle37; // 静态类型为Shape37*,动态类型为Rectangle37*
 
	pc->draw(Shape37::Red); // 调用Circle37::draw(Shape37::Red)
	pr->draw(Shape37::Red); // 调用Rectangle::draw(Shape37::Red)
 
	// 即使把指针换成references问题仍然存在
	pr->draw(); // 调用Rectangle37::draw(Shape37::Red)!
 
	return 0;
}
  • virtual函数系动态绑定(dynamically bound),而缺省参数值确是静态绑定(statically bound)。静态绑定又名前期绑定,early binding;动态绑定又名后期绑定,late binding。

  • 对象的所谓静态类型(static type),就是它在程序中被声明时所采用的类型。对象的所谓动态类型(dynamic type)则是指”目前所指对象的类型”。也就是说,动态类型可以表现出一个对象将会有什么行为。virtual函数系动态绑定而来,意思是调用一个virtual函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型。

请记住:绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数----你唯一应该覆写的东西----确是动态绑定。

条款38 通过复合塑模出has-a或”根据某物实现出”(Model “has-a” or “is-implemented-in-terms-of” through composition)

class Address {};
class PhoneNumber {};
class Person {
private:
	std::string name;        // 合成成分物(composed object)
	Address address;         // 同上
	PhoneNumber voiceNumber; // 同上
	PhoneNumber faxNumber;   // 同上
};
  • 复合(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。

  • public继承带有is-a(是一种)的意义。复合也有它自己的意义。实际上它有两个意义。复合意味has-a(有一个)或is-implemented-in-terms-of(根据某物实现出)。

请记住
(1). 复合(composition)的意义和public继承完全不同。
(2). 在应用域(application domain),复合意味has-a(有一个)。在实现域(implementation domain),复合意味is-implemented-in-terms-of(根据某物实现出)。

条款39 明智而审慎地使用private继承(Use private inheritance judiciously)

class Empty39 {};
class HoldsAnInt {
private:
	int x;
	Empty39 e;
};
 
class HoldsAnInt39 : private Empty39 { // EBO(empty base optimization)
private:
	int x;
};
 
int test_item_39()
{
	fprintf(stdout, "sizeof(Empty39): %d\n", sizeof(Empty39)); // 1
	fprintf(stdout, "sizeof(HoldsAnInt): %d\n", sizeof(HoldsAnInt)); // 8 
	fprintf(stdout, "sizeof(HoldsAnInt39): %d\n", sizeof(HoldsAnInt39)); // 4
 
	return 0;
}
  • 如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象。这和public继承的情况不同。由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原本是protected或public属性。private继承意味implemented-in-terms-of(根据某物实现出)。如果你让class D以private形式继承class B,你的用意是为了采用class B内已经备妥的某些特性,不是因为B对象和D对象存在有任何观念上的关系。private继承纯粹只是一种实现技术。private继承意味只有实现部分被继承,接口部分应略去。如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其它意涵了。

  • 尽可能使用复合,必要时才使用private继承。

  • EBO(empty base optimization, 空白基类最优化),EBO一般只在单一继承(而非多重继承)下才可行。

请记住
(1). private继承意味is-implemented-in-terms-of(根据某物实现出)。它通常比复合(composition)的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
(2).和复合(composition)不同,private继承可以造成empty base最优化。这对致力于”对象尺寸最小化”的程序库开发者而言,可能很重要。

条款40 明智而审慎地使用多重继承(Use multiple inheritance judiciously)

请记住
(1).多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。
(2).virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。
(3).多重继承的确有正当用途。其中一个情节涉及”public继承某个Interface class”和”private继承某个协助实现的class”的两相组合。

条款41 了解隐式接口和编译期多态(Understand implicit interfaces and compile-time polymorphism)

// 隐式接口
template<typename T>
void doProcessing(T& w)
{
	T someNastyWidget;
	if (w.size() > 10 && w != someNastyWidget) {
		T temp(w);
		temp.normalize();
		temp.swap(w);
	}
}
 
// 显示接口
class Widget41 {
public:
	Widget41();
	virtual std::size_t size() const;
	virtual void normalize();
	void swap(Widget41& other);
};
  • “以不同的template参数具现化function templates”会导致调用不同的函数,这便是所谓的编译期多态(compile-time polymorphism)。”哪一个重载函数该被调用”(发生在编译期)和”哪一个virtual函数该被绑定”(发生在运行期)。

  • 通常显式接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。隐式接口就完全不同了,它并不基于函数签名式,而是由有效表达式(valid expressions)组成。

请记住
(1).classes和templates都支持接口(interface)和多态(polymorphism)。
(2).对classes而言接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期。(3).对template参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。

条款42 了解typename的双重意义(Understand the two meanings of typename)

template<typename C>
void print2nd(const C& container)
{
	if (container.size() >= 2) {
		// 一般性规则很简单:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须
		// 在紧临它的前一个位置放上关键字typename
		typename C::const_iterator iter(container.begin());
	}
}
 
template<typename C> //允许使用"typename"(或"class")
void f42(const C& container, // 不允许使用"typename",C并不是嵌套从属类型名称
	typename C::iterator iter); // 一定要使用"typename",C::iterator是个嵌套从属类型名称
 
template<typename T>
class Base42 {
	class Nested {
		Nested(int x) {}
	};
};
 
template<typename T>
class Derived42 : public Base42<T>::Nested { // base class list中不允许"typename"
public:
	explicit Derived42(int x) 
	: Base42<T>::Nested(x) // mem.init.list中不允许"typename"
	{
		typename Base42<T>::Nested temp; // 嵌套从属类型名称,既不在base class list中也不在
			// mem.init.list中,作为一个base class修饰符需加上typename
	}
};
 
template<typename IterT>
void workWithIterator(IterT iter)
{
	// 使用IterT对象所指物的相同类型将temp初始化为iter所指物
	// 如果IterT是vector::iterator,temp的类型就是int
	// value_type被嵌套于iterator_traits之内而IterT是个template参数,所以必须在它之前放置typename
	typename std::iterator_traits<IterT>::value_type temp(*iter);
	
	typedef typename std::iterator_traits<IterT>::value_type value_type;
	value_type temp2(*iter);
}
  • 当我们声明template类型参数,class和typename的意义完全相同。

  • template内出现的名称如果相依于某个template参数,称之为从属名称(dependent names)。如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称(nested dependent name)。嵌套从属类型名称(nested dependent type name),也就是个嵌套从属名称并且指涉某类型。

  • tepename只被用来验明嵌套从属类型名称,其它名称不该有它存在。

  • “typename必须作为嵌套从属类型名称的前缀词”这一规则的例外是,typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为base class修饰符。
    请记住
    (1).声明template参数时,前缀关键字class和typename可互换。
    (2).请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。

条款43 学习处理模板化基类内的名称(Know how to access names in templatized base classes)

你可能感兴趣的:(Cpp,Learning,Road,c++,开发语言,c#,java)