读书笔记:Effective C++ 3.0版2005年Scott Meyers : 55条建议(1-46)

Effective C++第一版大约在1990年,第二版1997年,第三版2005年。
第四版变了名字《Effective Modern C++》,因为c++11、c++14变更较大,c++17、c++23对应的似乎还没有。
其他参考书:《Effective STL》、More Effective C++。

以下为概要学习笔记,如果太长就分篇:

软件的目标:正确、时间空间高效、稳定、易理解维护、易扩展、可移植、与底层和上层匹配…

X t =t1; // 调用的是拷贝构造函数,并不是operator=拷贝赋值函数。

拷贝构造函数在传值参数时需要。void api(X t);
个人习惯是为类写好所有范式函数,避免遇到模板编译不通过、非预期行为这种问题。

ctor 构造函数。dtor 析构函数。

lhs left-hand side , rhs right-hand side

a*b
operator*(a,b);
const X operator*(const X &lhs,const X &rhs);//二元操作符定义范式

c++出现的早期,没有multithreaded多线程概念,所以早期c++实际天然地不支持多线程。

TR1(Technical Report 1)是类似Boost的一个库。注意这本书是2005年的,有些库已经过时了。

条款01 :c++是一个多语言联邦。
主要次语言概念:c(procedural过程)、class(object-oriented面向对象)、template(generic泛型metaprogramming元编程)、函数形式functional、stl库。不同范畴的规则范式建议各有区别,关键看本质。

stl迭代器、函数对象,本质是指针。
内置类型传值比较好,有些是cpu指令集所支持的,例如c语言里面的复数。
自定义类型,较大对象建议传引用、本质是指针。

条款02 :尽量用const、enum、inline替代#define
单纯的常量,尽量用const、enum。类似函数的宏改用inline。
本质是面向编译器编程,减少对预处理器的依赖。
编译器有更多检查,更安全。预处理器简单粗暴,缺乏安全性。
#define被预处理器替换后,缺乏符号表,记号式调试器无法体现,不容易看出问题。

#define PI 3.14159265358979323846264338327650288419716939937510
const double PI=3.14159265358979323846264338327650288419716939937510; //推荐
const char* const s="PI";
const std::string s("PI"); //推荐
class X{
enum{kI=42};//用枚举可以定义常量
//__LINE__也可用于定义常量
private:
static const int i=42; //h中,新版cpp编译器支持h中给出整数初值,浮点数不行
static const double d;
};
const int X::i;//cpp中
const int X::d=4.2;
#define Max(a,b) (a)>(b)?(a):(b)  //废弃
template<typename T>inline const T& max(const T& a,const T& b){return a>b?a:b;} //推荐

用__LINE__和函数内无名enum推算常量,用于定义固定长度数组
关键点在于:利用编译器在编译期,通过函数内无名enum和__LINE__自动获得函数内变量总量。

enum hack是模板元编程template metaprogramming的基础。

条款03 :尽量用const
本质是面向编译器编程,概念清晰一致。
告诉编译器更多的程序概念细节,方便优化、减少错误。
注意新的关键字,mutable可变的,与const对应。
const函数中可以修改mutable成员。

class X{
	mutable std::size_t i;
	std::size_t length()const{ //const函数
		i = std::strlen(p); //修改了mutable成员
		return i;
	}
};

另外,const本质是编译期的事情,告诉编译器如何更好地处理代码。
所以就有剔除const修饰的语法支持

const T& operator[](std::size_t i)const{return v[i];}
T& operator[](std::size_t i){
	return 
		const_cast<T&>(                        //剔除const修饰
			static_cast<const TT&>(*this)      //加上const修饰,使得自动调用const operator[]
				[i]                            //调用const operator[]
		);
}

const_cast用于修改值的const或volatile属性,删除const或者添加const属性,实际主要是用于一种情况:删除const属性。
static_cast用于在编译时可以确定的类型转换,支持派生类到基类,不支持基类到派生类。
dynamic_cast用于需要运行时信息来确定是否可以执行的类型转换,支持基类到派生类。
c风格的转换也是可以的,但是没有cpp的安全,毕竟cpp的信息更多更明确。
非const函数可以调用const函数,安全级别低的调高的。反之则不合适,概念上就出现问题了。

条款04 :确定对象被使用前已经被初始化
int i = 0; // 初始化使得概念明确化,避免不确定的初始化值引起bug。
c array不保证初始化,cpp vector保证了初始化,存在运行期成本的差别,各有取舍。
如果是超大内存,这事又另当别论。
cpp class每个构造函数都保证初始化每个成员,且用初始化列表,而不是赋值。
基类先初始化,派生类在后面初始化。
成员按照定义顺序初始化,int size;与T v[];定义时就要注意顺序。
static对象会在main函数结束后析构。
用函数内的局部static变量控制它们的初始化时序;如果有多线程隐患,那就在初始阶段单线程调用一遍。

条款05 :了解C++默默编写并调用哪些函数(Know what functions C++ silently writes and calls)
编译期自动补全的函数只有在被使用时才会被创建。
类似模板函数的实例化,没有用过的类型版本是不会被创建的。
析构函数默认是非虚版本,原因是降低成本(虚函数表的空间占用、运行时开销)。
如果含有引用成员、const成员等,就没有默认的成员拷贝,涉及引用不可变等问题。

class X{}; //你看到的版本
class X{   //实际的版本,编译期自动补全的版本
public:
	inline X(){}			 //默认构造函数。X x;触发。如果定义了其他版本,就不会有默认版本。
	inline X(const X &t){}  //拷贝构造函数。X x(t);触发。
	inline ~X(){}			 //析构函数,非虚。X x;触发。
	//virtual ~X(){}		 //虚析构函数。当基类有虚析构函数时才会是这种版本。
	inline X&operator=()(const X &t){} //拷贝赋值,x=t;触发
};

个人认为这一条的本质是:编译器会尽量帮着做所有可能做的事情,安全高效省事。

条款06 :若不想使用编译器自动生成的函数,就该明确拒绝。(Explicitly disallow the use of compiler-generated functions you do not want)
核心是:准确表达概念,不应有歧义。
以下形成Uncopyable模式。同时给出新旧两种方案。

class Uncopyable{
protected: //防止X被直接使用,此处protected可以使得X成为一种模式,仅仅是赋予派生类不可拷贝的模式。
	Uncopyable() {}
	~Uncopyable() {}
private: //通过私有函数阻止拷贝构造和赋值拷贝,旧方案,实际对于friend类存在隐患。
	Uncopyable(const Uncopyable&);
	Uncopyable& operator= (const Uncopyable&);
protected: //以下为c++11的语法新方案,不受访问级别影响
    Uncopyable() = default;   // 使用=default来明确表示使用默认的构造行为
    ~Uncopyable() = default;    
    Uncopyable(const Uncopyable&) = delete;// 显式地删除拷贝构造函数和拷贝赋值运算符
    Uncopyable& operator= (const Uncopyable&) = delete;
};

private:只有同一个类的成员函数可以访问此成员。
protected:同一个类的成员函数和其子类(派生类)可以访问此成员。
public:任何函数都可以访问此成员。

条款07 :为多态基类声明virtual析构函数(Declare destructors virtual in polymorphic base classes)
delete基类或者派生类指针时,如果没有虚析构函数,只会调用当前delete语句中的类型析构,否则就从实际new出来的析构开始调用(查虚函数表)。

template <class _CharT, class _Traits, class _Alloc> 
class basic_string : private _String_base<_CharT,_Alloc>

std中的string、basic_string、_String_base都没有虚函数。因为std string的设计初衷里面就没有多态polymorphically。
polymorphic(带多态性质的)base classes的设计目的是为了用来”通过base class接口处理derived class对象”,但std中的_String_base并不是。
vptr(virtual table pointer)指向一个由函数指针构成的数组,称为vtbl(virtual table)。每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl----编译器在其中寻找适当的函数指针。个人认为是在new的时候决定的vptr值。
另外,对于小对象,也尽量不要用虚函数,否则内存消耗增大50%甚至100%,对于海量对象就是一个灾难。
TODO:std中无虚函数的基类模式,设计思想。估计是只会对派生类进行简单new delete,不涉及内存泄漏。这种级别的源码往往要求编译器也配合语法支持,甚至msvc还有自己的语法。核心是语义的表达和编译支持。

条款08 :别让异常逃离析构函数(Prevent exceptions from leaving destructors)
cpp编译期并不禁止析构函数吐出异常,但不建议,因为array的析构异常会导致内存泄漏。
解决方法:在析构函数中try catch,处理异常,std::abort()或输出错误日志。
或者将可能出现异常的行为交由用户决断、调用,用户可单独调用的函数处理可能有异常的事务,析构函数只做最后的防御。

class DBConn {
public:
	void close(){//供客户使用的新函数
		db.close( );closed = true;
	}
	~DBConn(){
		if (!closed){
			try{
				db.close();//关闭连接(如果客户不那么做的话)
			}catch (...) {//如果关闭动作失败
				//制作运转记录,记下对 close 的调用失败.				
				//记录下来并结束程序,或吞下异常。
			}
		}
	}
private:
	DBConnection db;
	bool closed;
};

条款09 :绝不在构造和析构过程中调用virtual函数(Never call virtual functions during construction or destruction)
因为对象构造析构的时序问题,构造和析构过程中调用virtual函数可能会导致访问未初始化或者已经释放的对象。
另外,base class构造期间,虚函数不是虚函数,甚至类型信息也是基类信息。
另外,间接调用也不行。

条款10 :令operator=返回一个reference to *this(Have assignment operators return a reference to *this)
不遵循return *this也可以,只是不符合约定俗成。

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

条款11 :在operator=中处理”自我赋值”(Handle assignment to self in operator=)
防止bug的一种编码范式。实际并不一定只有operator=存在这种潜在的bug,注意考虑这种特例。

Widget& Widget::operator=(const widget& rhs){
	if (this == &rhs) return *this;//证同测试(identity test) : 如果是自我赋值,就不做任何事
	delete pb;
	pb = new Bitmap(*rhs.pb);
	return *this;
}

条款12 :复制对象时勿忘其每一个成分(Copy all parts of an object)
除了当前类的,还需要关注基类的,BaseClass::operator=(rhs);

条款13 :以对象管理资源(Use objects to manage resources)
资源:堆栈内存、文件、互斥锁、UI中的字体笔刷、数据库链接、网络sockets等等。
把资源放进对象内,利用C++的”析构函数自动调用机制”确保资源被释放。
auto_ptr是个”类指针(pointer-like)对象”,也就是所谓的”智能指针”,其析构函数自动对其所指对象调用delete。一定要注意别让多个auto_ptr同时指向同一对象,如果真是那样,对象会被删除一次以上。
在C++11中auto_ptr已经被废弃,用unique_ptr替代。auto_ptr不支持部分std容器。
shared_ptr允许多个指针指向同一个对象,但存在环状引用问题。
unique_ptr则"独占"所指向的对象。”所有权”仅能够通过标准库的move函数来转移。unique_ptr是一个删除了拷贝构造函数、保留了移动构造函数的指针封装类型。

    template <class _Ty, class _Dx /* = default_delete<_Ty> */>
    class unique_ptr{
    public:
    ...
    	unique_ptr(const unique_ptr&)            = delete;
    	unique_ptr& operator=(const unique_ptr&) = delete;
    ...
    };

RAII(Resource Acquisition Is Initialization,资源取得时机便是初始化时机)对象,它们在构造函数中获得资源并在析构函数中释放资源.
”引用计数型智慧指针”(reference-counting smart pointer, RCSP)。所谓RCSP也是个智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。类似于C++11中的shared_ptr。

条款14 :在资源管理类中小心copying行为(Think carefully about copying behavior in resource-managing classes)
(1).复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。(2).普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法(reference counting)。不过其它行为也都可能被实现。
这一条也是内存管理的建议,比较基础和常规。

#include <iostream>
#include <memory>
class ResourceManager {
private:
    int* resource;
public:
    ResourceManager() : resource(new int(5)) {}
    ~ResourceManager() { delete resource; }
    ResourceManager(const ResourceManager& other) : resource(new int(*other.resource)) {}
    ResourceManager& operator=(const ResourceManager& other) {
        if (this != &other) {
            delete resource;
            resource = new int(*other.resource);
        }
        return *this;
    }
    // 示例中的RAII类的另一种选择是禁止复制
    // ResourceManager(const ResourceManager& other) = delete;
    // ResourceManager& operator=(const ResourceManager& other) = delete;
};

条款15 :在资源管理类中提供对原始资源的访问(Provide access to raw resources in resource-managing classes)
请记住:(1).APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个”取得其所管理之资源”的办法。(2).对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。

#include <iostream>
#include <cstring>
#include <memory>
class StringResource {
private:
    std::unique_ptr<char[]> str;
public:
    StringResource(const char* s) : str(new char[strlen(s) + 1]) {
        strcpy(str.get(), s);
    }
    const char* getRawString() const {return str.get();}
    operator const char* () const {return str.get();}
};

条款16 :成对使用new和delete时要采用相同形式(Use the same form in corresponding uses of new and delete)
这个的原因在之前的文章有讲过,https://blog.csdn.net/weixin_43172531/article/details/133872314
Effective C++ 2.0 版,条款5:对应的new和delete要采用相同的形式
本质是new[]时,在指针前面有16byte用于管理数组大小,编译期生成了不同的代码和数据结构。
条款17 :以独立语句将new对象置入智能指针(Store newed objects in smart pointers in standalone statements)
以独立语句将new对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。

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;
}

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

class CustomString {
public:
    // 可以通过复制构造函数和赋值操作符对接口进行一致性设计
    CustomString(const char* str);
    CustomString(const CustomString& other);
    CustomString& operator=(const CustomString& other);
    
    // 与内置类型兼容的显示转换
    explicit operator const char*() const;
};

void processString(const CustomString& str);
class NonCopyableResource {
public:
    NonCopyableResource();
    NonCopyableResource(const NonCopyableResource&) = delete; // 删除复制构造函数
    NonCopyableResource& operator=(const NonCopyableResource&) = delete; // 删除赋值操作符
};
void closeFile(FILE* file) {
    // 使用std::shared_ptr并传入lambda作为定制型删除器
    std::shared_ptr<FILE> filePtr(file, [](FILE* f) { fclose(f); });
    // 无需手动关闭文件,离开作用域后自动调用fclose
}

条款19 :设计class犹如设计type(Treat class design as type design)

class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};
class Circle : public Shape {
public:
    double area() const override {        return 3.14 * radius_ * radius_;    }
private:
    double radius_;
};
class Square : public Shape {
public:
    double area() const override {        return side_ * side_;    }
private:
    double side_;
};

class Email {
public:
    explicit Email(const std::string& address) : address_(address) {}
    std::string getAddress() const { return address_; }
private:
    std::string address_;
};
void sendEmail(const Email& email) {
    // 发送电子邮件
}

条款20 :以pass-by-reference-to-const替换pass-by-value(Prefer pass-by-reference-to-const to 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往往比较适当。
切割问题(slicing problem)通常出现在传递派生类对象的基类接口时,使用值传递的情况下。

//Shape是基类,派生类对象传进来时变成了基类对象,派生类的信息被切割了。
void drawShape(Shape s) { 
    s.draw();
}

条款21 :必须返回对象时,别妄想返回其reference(Don’t try to return a reference when you must return an object)
绝不要返回pointer或reference指向一个local stack对象,本地栈对象。
或返回reference指向一个heap-allocated对象,堆上申请的对象。
或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
实际还有一个原因:返回引用可能会被滥用,导致值被莫名其妙地改变。虽然可以提高效率,但可能要根据具体情况谨慎使用。

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

条款23 :宁以non-member、non-friend替换member函数(Prefer non-member non-friend functions to member functions)
宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。

class Point {
public:
    float x, y;
};
// 非成员非友元函数
void offset(Point& point, float offsetX, float offsetY) {
    point.x += offsetX;
    point.y += offsetY;
}

类似std的几十个头文件,目标是分开定义,最小化编译。

条款24 :若所有参数皆需类型转换,请为此采用non-member函数(Declare non-member functions when type conversions should apply to all parameters)
无论何时如果你可以避免friend函数就该避免。
如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。

class Rational {//有理数
public:
    Rational(int numerator = 0, int denominator = 1) : n(numerator), d(denominator) {} 
    int numerator() const { return n; }//分子
    int denominator() const { return d; }//分母
private:
    int n, d;
};
// Operator overloads implemented as non-member functions
const Rational operator*(const Rational& lhs, const Rational& rhs) {
    return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
} 
int test(){
	Rational24 oneEighth(1, 8);
	Rational24 oneHalf(1, 2);
	Rational24 result = oneHalf * oneEighth;
	result = result * oneEighth;
	result = oneHalf * 2; // 隐式类型转换(implicit type conversion)
	result = 2 * oneHalf; // 也可以隐式类型转换
	return 0;
}

条款25 :考虑写出一个不抛异常的swap函数(Consider support for a non-throwing swap)
缺省情况下swap动作可由标准程序库提供的swap算法完成。
(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而言全新的东西。

class WidgetImpl { // 针对Widget数据而设计的class
private:
	int a, b, c; // 可能有许多数据,意味复制时间很长
	std::vector<double> v;
}; 
class Widget { // 这个class使用pimpl(pointer to implementation)
public:
	Widget(const Widget& rhs) {}
	Widget& operator= (const Widget& rhs)	{
		*pImpl = *(rhs.pImpl); 
		return *this;
	} 
	void swap(Widget& other)	{
		using std::swap;
		swap(pImpl, other.pImpl);
	} 
private:
	WidgetImpl* pImpl;
}; 
namespace std {
template<>
void swap<effective_cplusplus_::Widget25>(effective_cplusplus_::Widget25& a, effective_cplusplus_::Widget25& b)
{
	a.swap(b);
} 
}

条款26 :尽可能延后变量定义式的出现时间(Postpone variable definitions as long as possible)
减少构造析构的调用,毕竟有可能执行不到那个定义的位置。可增加程序的清晰度并改善程序效率。

条款27 :尽量少做转型动作(Minimize casting)
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风格转型)。前者很容易辨识出来,而且也比较有着分门别类的职掌。
使用explicit构造函数时,需要使用c风格转换。

条款28 :避免返回handles指向对象内部成分(Avoid returning “handles” to object internals)
避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生”虚吊号码牌”(dangling handles)的可能性降至最低。
dangling handles实例:外部指针指向内部对象,内部对象失效时,外部指针成为野指针。
实际性能优先时,这一条也不是不能破例:

#include <cstdint>  // for uint8_t
#include <cstddef>  // for size_t
class file_view{
public:
    typedef uint8_t*      uint8_pointer;
    typedef uint8_pointer& uint8_pointer_ref;
    enum : uint8_t{
        kNewLineFlag_0x0D = 0x0D,
        kNewLineFlag_0x0A = 0x0A,
    };
public:
    file_view(uint8_t* const file_pointer, const size_t file_size)
        : file_pointer_(file_pointer), file_size_(file_size), size_parsed_(0)
    {}
    file_view()  = delete;
    ~file_view() = default;
    inline bool get_line(
        uint8_pointer_ref p_start,
        uint8_pointer_ref p_end,
        const uint8_t    new_line_flag = kNewLineFlag_0x0A){
        if (size_parsed_ >= file_size_){
            p_start = p_end = nullptr;
            return false;
        }
        uint8_t* p_cur = file_pointer_ + size_parsed_;
        while (size_parsed_ < file_size_ && kNewLineFlag_0x0D == *p_cur){
            ++p_cur;
            ++size_parsed_;
        }
        while (size_parsed_ < file_size_ && *p_cur == new_line_flag){
            ++p_cur;
            ++size_parsed_;
        }
        p_start = p_cur;
        while (size_parsed_ < file_size_ && *p_cur != new_line_flag){
            ++p_cur;
            ++size_parsed_;
        }
        p_end = p_cur;
        return true;
    }
private:
    uint8_t* const file_pointer_;  // 文件指针
    const size_t   file_size_;     // 文件总大小
    size_t         size_parsed_;   // 已解析的size
};

条款29 :为”异常安全”而努力是值得的(Strive for exception-safe code)
“异常安全”有两个条件:(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).函数提供的”异常安全保证”通常最高只等于其所调用之各个函数的”异常安全保证”中的最弱者。

#include <iostream>
#include <cstring>
class MyString {
public:
    MyString() : data(nullptr), length(0) {}
    MyString(const char* str) : data(nullptr), length(0) {
        if (str) {
            length = std::strlen(str);
            data = new char[length + 1];
            std::strcpy(data, str);
        }
    }
    MyString(const MyString& other) : MyString() {
        MyString temp(other.data); // Use the constructor to create a copy
        swap(temp); // Swap the content of this and temp
    }
    MyString(MyString&& other) noexcept : data(nullptr), length(0) {
        swap(other); // Swap the content with the temporary object
    }
    ~MyString() {
        delete[] data;
    }
    MyString& operator=(MyString other) {
        swap(other); // Swap the content with the copy
        return *this;
    }
    // Swap function
    void swap(MyString& other) noexcept {
        std::swap(data, other.data);
        std::swap(length, other.length);
    }
    const char* c_str() const {
        return data;
    }
private:
    char* data;
    std::size_t length;
};
int main() {
    MyString str1("Hello");
    MyString str2("World");
    // Copy and Swap used in assignment
    str1 = str2;
    std::cout << "str1: " << str1.c_str() << std::endl; // Output: "str1: World"
    return 0;
}

条款30 :透彻了解inlining的里里外外(Understand the ins and outs of inlining)
(1).将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。(2).不要只因为function templates出现在头文件,就将它们声明为inline。较小模板函数会被编译期自动inline,交由编译器自行判断。
单文件时,所有函数inline可以简单解决编译问题。
inline的函数不易被debug。
代码inline导致过大,换页更频繁,也会导致速度下降,不是所有inline都能提速。

条款31 :将文件间的编译依存关系降至最低(Minimize compilation dependencies between files)
(1). 支持”编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是handle classes和interface classes。(2). 程序库头文件应该以”完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。
减少编译成本,低耦合问题。
尽量做到:少一行就编译失败,多一行就浪费,每行代码都有底层逻辑在支撑。

条款32 :确定你的public继承塑模出is-a关系(Make sure public inheritance models “is-a”)
”public继承”意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。

条款33 :避免遮挡继承而来的名称(Avoid hiding inherited names)
(1). derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
(2).为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding functions)。

条款34 :区分接口继承和实现继承(Differentiate between inheritance of interface and inheritance of implementation)
(1).接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
(2). pure virtual函数只具体指定接口继承。
(3). 简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。
(4). non-virtual函数具体指定接口继承以及强制性实现继承。

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

class Base {
public:
    void operation() {
        // Non-virtual interface
        doOperation();
    }
private:
    virtual void doOperation() = 0;
};
class Derived : public Base {
private:
    void doOperation() override {
        // Implementation for Derived
    }
};
class Strategy {
public:
    virtual void execute() = 0;
};
class ConcreteStrategyA : public Strategy {
public:
    void execute() override {
        // Implementation for Strategy A
    }
};
class ConcreteStrategyB : public Strategy {
public:
    void execute() override {
        // Implementation for Strategy B
    }
};
class Context {
private:
    Strategy* strategy;
public:
    void setStrategy(Strategy* s) {
        strategy = s;
    }
    void executeStrategy() {
        strategy->execute();
    }
};
class MyClass {
private:
    int privateData;
public:
    friend void externalFunction(MyClass& obj);
};
void externalFunction(MyClass& obj) {
    int data = obj.privateData;
}
#include <functional>
class Base {
public:
    std::function<void()> operation;//本质就是函数指针
};
class Derived : public Base {
public:
    Derived() {
        operation = []() {//lambda 表达式本质是函数指针,或者有inline,具体看编译期
            // Implementation for Derived
        };
    }
};

条款36 : 绝不重新定义继承而来的non-virtual函数(Never redefine an inherited non-virtual function)
non-virtual函数都是静态绑定(statically bound).
virtual函数确是动态绑定(dynamically bound)。

条款37 : 绝不重新定义继承而来的缺省参数值(Never redefine a function’s inherited default parameter value)
virtual函数系动态绑定(dynamically bound),
而缺省参数值确是静态绑定(statically bound)。
静态绑定又名前期绑定,early binding;动态绑定又名后期绑定,late binding。

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

条款38 : 通过复合塑模出has-a或”根据某物实现出”(Model “has-a” or “is-implemented-in-terms-of” through composition)
(1). 复合(composition)的意义和public继承完全不同。
(2). 在应用域(application domain),复合意味has-a(有一个)。
在实现域(implementation domain),复合意味is-implemented-in-terms-of(根据某物实现出)。

条款39 : 明智而审慎地使用private继承(Use private inheritance judiciously)
如果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)
(1).classes和templates都支持接口(interface)和多态(polymorphism)。
(2).对classes而言接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期。
(3).对template参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。

条款42 :了解typename的双重意义(Understand the two meanings of typename)
(1).声明template参数时,前缀关键字class和typename可互换。
(2).请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。

条款43 :学习处理模板化基类内的名称(Know how to access names in templatized base classes)
可在derived class templates内通过”this->”指涉base class templates内的成员名称,或藉由一个明白写出的”base class资格修饰符”完成。

在 C++ 中,模板是在编译时实例化的,而不是在运行时。当你定义一个模板化类时,编译器并不知道具体的模板参数类型,因此无法确定成员名称是否存在,以及如何访问它们。

资格修饰符 (Qualification): 使用 this-> 或 Base:: 等资格修饰符来明确指示你正在访问的成员属于哪个类。这种方式可以让编译器知道你的意图,从而正确解析成员名称。

依赖名字查找 (Dependent Name Lookup): 当模板类内部引用成员名称时,编译器将这些名称视为依赖于模板参数的名称。这些名称的解析会推迟到实例化阶段,当编译器获得了具体的模板参数类型信息时才进行。

由于 C++ 的模板机制的复杂性,编译器在模板实例化时必须考虑不同的上下文和可能的参数类型,因此成员名称不会自动可见。为了确保模板代码的正确性和可移植性,需要显式指定名称的所属类或使用资格修饰符。

虽然这可能会增加一些额外的编写代码的复杂性,但它也提供了更大的灵活性,因为它允许你在不同的上下文中使用相同的模板类,并且可以根据实际需要访问不同的成员。

条款44 :将与参数无关的代码抽离templates(Factor parameter-independent code out of templates)
(1).Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
(2).因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。例如整数作为函数参数比较合适。
(3).因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representations)的具现类型(instantiation types)共享实现码。

感觉要通过多看std、boost等优秀代码去找到感觉,也需要知道编译器是怎么去编译的。

条款45 :运用成员函数模板接受所有兼容类型(Use member function templates to accept “all compatible types”)

template<typename T>
class SmartPtr {
public:
	// 需要支持U* T*转换
	template<typename U>
	SmartPtr(const SmartPtr<U>& other) : heldPtr(other.get()) {} 
	T* get() const { return heldPtr; } 
private:
	T* heldPtr;
};

条款46 :需要类型转换时请为模板定义非成员函数(Define non-member functions inside templates when type conversions are desired)

#include <iostream>
template <typename T>
class XXX {
public:
    XXX (T value) : data(value) {}
    T getData() const {
        return data;
    }
private:
    T data;
};
// 非成员函数用于类型转换
template<typename T>
const XXX<T> doMultiply(const XXX <T>& lhs, const XXX <T>& rhs){
	return XXX <T>(lhs.getData() * rhs.getData());
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值