More Effective C++

More Effecctive C++

基本议题:Basics

条款1: 仔细区别pointersreferences

由于reference必须代表某个对象,因此必须被初始化。由此,reference不会成为null

但指针有这种可能性。

  • pointers可以不被赋初值
  • pointers可以被重新赋值

条款2:最好使用C++转型操作符

四种

  • static_cast<>(): 常用于隐式类型转换
  • const_cast<>():去除或添加常量性
  • dynamic_cast<>() 用于多态的类型中,基类与子类的互转。
  • reintepret_cast<>():最常用于函数指针转换。不具有移植性。

条款3:绝对不要以多态(polymorphically)方式处理数组

数组是根据当前位置,数组存放对象的大小,来计算下一个对象的位置。

继承体系中,子类很有可能会比基类占用更大内存。

因此,当使用多态方式处理数组时,内存偏移量很可能是错误的,无法找到下一个下标的对象的位置。

条款4: 非不要不提供default constructor

在一定的场景下,没有指定的信息,构造出来的对象是无意义的。

操作符:Operators

条款5: 对定制的“类型转换”函数保持警觉

类型转换函数有转换构造和操作符转换等。尽可能声明为explicit来避免非预期的隐式转换。(可以主动使用static_cast转换)

class obj{
public:
    explicit obj(int val): val(val) {}
    explicit operator double() {
        return val * 10.1;
    }
private:
    int val;
};

条款6:区分incrementdecrement的前置和后置形式

前置和后置形式都应该以前置式为基础,只需维护前置形式即可。

条款7:千万不要重载&&,||操作符

就算可以被重载,也不应该无理由的进行重载。

因为无法令其行为像他们内置的行为一样(执行顺序,短路性质)。

*条款8:了解各种不同意义的 newdelete*

operator newnew operator是不一样的。我们经常使用到的newnew opeartor。它实际包括了,分配内存和构造对象两个过程。这个是不能重载的。

但可被显示分为两个过程,一个是分配内存(operator new),一个是构造对象(placement new)。

  • 我们能重载的是operator new,这个函数仅仅负责分配内存,接收size_t类型,返回指向内存的void* 指针。
  • placement new无法被重载。

由于new operator会调用上面两个过程,因此我们可以重载operator new来自定义new operator部分行为。

同时,由于可以将分配内存和构造对象分开。在需要多次使用一个临时对象的场景下,可以开辟一个该空间重复构造。从而节省大量分配内存,释放内存的时间

同理,delete operator也分为析构对象和释放内存两个过程。对于不同分配方式的内存,需要不同地释放(operator newoperator delete是对应的, placement new出来的对象需要手动析构)。

析构对象

  • 当使用了placement new的时候,不能对那块内存使用delete operator,因为delete opertor会调用operator delete来释放内存。
  • 但该内存的对象并非由operator new分配而来。因此对于placement new,我们只需要调用其对象的析构函数即可。

释放内存

  • heap内存就operator delete(ptr);
  • stack内存就无需操作。

数组的new和delete遵循即可。

  • new对应delete
  • new[] 对应 delete[]

异常:Exception

条款9:利用destructors避免泄露资源

异常发生时,普通指针的delete操作可能不会被正常执行。

可以将其delete操作置于一个局部变量的destructor中。局部变量在离开作用域时会被析构。

即,smart pointer。 用智能指针的destructots释放其所指的heap内存。

条款10:在constructors内阻止资源泄露(rescource leak

当内部需要heap资源时,需要注意,当构造函数中发生异常,可能会对已经分配好的heap资源无法析构释放。

理论上,该类的资源应该由该类的析构函数释放。但由于该对象未成功构造,因此无法被析构(C++规定不那么做)。

可以使用智能指针代替内置指针管理该类的成员,自动释放内存。

条款11:禁止异常流出destructors之外

有两点好处

  • 避免terminate函数在exception传播过程的栈展开(stack-unwinding)机制中被调用。
  • 保证destructor的所有步骤被执行。

条款12:了解”抛出一个exception与”传递一个参数“或调用一个”虚函数“之间的差异

  • exception objects总是会被复制(第一次抛出时一定被复制)。如果是以pass by value,会被复制更多次。
  • 总共有三种传递方式catch by value\ by reference\ by pointer.(pass by value时可以是non-const,但函数传参不行)。
  • 抛出的exception对象被允许的转型动作,比”被传递到函数去“的对象少。
  • 抛出的exception对象pass by value时,总是根据其静态类型来copy。也有例外,见条款25。
  • exception objects匹配顺序由catch顺序决定。

条款13:以by reference方式捕捉exception

三种方式比较

catch by pointer

如果指向了局部变量,则导致undefined behavior,如果指向heap变量,便要纠结是否要删除该变量回收内存。

因此,不推荐这种方式。

catch by value

可以消除上述是否需要删除和局部变量的问题。但是,也会导致切割(slicing)。如

子类被基类捕获,则切割了子类特有部分。因此,调用的函数还是其基类的。

catch by reference

解决了catch by pointer的变量生存期和catch by value的slicing问题。

可以通过虚函数调用正确版本。

条款14:明智运用exception specifications

懂了大概意思,有些抽象。难以总结。

条款15:了解异常处理(exception handing)的成本

效率:Efficiency

条款16:谨记80-20法则

针对20的代码做优化,那才是效率瓶颈。

选用更好的分析器并尽可能以最多的数据分析你的软件。

条款17:考虑使用lazy evaluation

需要用到时才进行相应操作。需要搭配其他技术如代理(proxy

条款18:分期摊还预期成的计算成本

Over-eager evaluation

  • Caching:使用Cache缓存
  • Prefetching:一次取出磁盘的大部分内容(可能他们都会被用到),而不是一块一块地读(改随机读为顺序读);vector扩张时直接扩1.5倍。

条款19: 了解临时对象的来源

局部对象(如用于swap交换的temp变量)不是临时变量。临时变量来源有

  • 隐式类型转换
  • 返回值

注意reference-to-const参数,常常用于临时变量。但reference-non-const则不会(编译器认为改变临时变量达不到程序员的预期,因此拒绝)。

条款20:协助完成”返回值优化“(RVO) return value optimization

编译器为节pass by value的开支,优化了临时变量作为返回值(相当于直接构造)。

条款21:利用重载(overlaod)避免隐式类型转换(implicit type conversions

注意,重载运算符时,必须至少有一个是非内置类型。

比如不能定义Complex operator+(int lhs, int rhs);。改变了int加法的本来含义。

条款22:考虑以操作符复合形式(op=)取代其独身形式(op)

通常独身形式一般基于符合形式实现,继续维护复合形式。

因此,复合形式具有更高效率。且,返回时,优先选择匿名对象。

条款23:考虑使用其他程序库

如stdio和iostream库的对比

条款24:了解virtual functionmultiple inheritancevirtual base classesruntime type identification的成本

对于多态,大部分编译器使用虚表(virtual tables)和虚表指针(virtual table pointers)实现。

具体实现见《深度探索C++对象模型》(Inside the C++ Object Model byStanley B.Lippman)

技术: Techniques, Idioms, Patterns

条款25:将constructor虚化和non-member functions虚化

并非是把constructor声明为virtual,而是使用虚函数来实现virtual constructor的功能。

考虑这样一个场景。Base为消息,Derive有视频消息,文字消息等。使用基类指针来拷贝实际类型,就要根据实际类型来拷贝,即虚函数。

copy constructor

class Base{
public:
    virtual Base* clone() const = 0;
};

class Derive1: public Base{
public:
    virtual Derive1* clone() const {
        return new Derive1(*this);
    }
};

class Derive2: public Base{
public:
    virtual Derive2* clone() const {
        return new Derive2(*this);
    }
};

non-member function

class Base{
public:
    virtual ostream& print(ostream& s) const = 0;
};

class Derive1: public Base{
public:
    virtual ostream& print(ostream& s) const {
        s << "Derived1" << endl;
    }
};

class Derive2: public Base{
public:
    virtual ostream& print(ostream& s) const {
        s << "Derived2" << endl;
    }
};

inline ostream& operator<<(ostream& s, const Base& o) {
    return o.print(s);
}

条款26:限制某个class所能产生的对象

实现单例模式的一种简单想法是,将构造函数声明为private或 = delete,使用一个static函数(以下称为工厂函数)来创建该类的static对象,用于返回该类对象的引用。

工厂函数不能声明为inline,因为inline会在多处展开,生成多个副本,就破坏了单例。(注,后续已经修复。无需考虑这点问题)

另外一种限制数量的方式是,使用一个计数器。超过即抛出异常。仅仅从自身角度来说,这是可行的。

但要考虑其他两种情况。派生出子类被其他类包含。这样,也会产生计数器的计数(调用了构造函数),就算他理论上不应该被计数(不是同一类东西)。

因此,需要将构造函数声明为private,结合工厂函数和智能指针管理。

另一种实现方式是,使用一个用于计算对象个数的Base Class,采用private inheritance,即has-a的形式。

条款27:要求(或禁止)对象产生于heap之中

太抽象了,且用法都比较Trick不具有移植性。

条款28:智能指针(Smart Pointers

测试Smart Pointer是否为nullptr

一种做法是提供一个isNull函数,但为了尽可能模仿原指针的行为,另一种做法是,进行隐式转换。

但转换为其他类型(如boolvoid*等)都不能避免不同类型的互相比较问题(由于隐式转换可能会转成相同的类型)。

C++标准库中,隐式转换为void*已经被bool取代,而operator bool总是返回operator!的反相。

源码中与nullptr的比较。

typedef decltype(nullptr) nullptr_t;
template<typename _Tp>
  inline bool
  operator==(const shared_ptr<_Tp>& __a, nullptr_t) noexcept
  { return !__a; }

template<typename _Tp>
  inline bool
  operator==(nullptr_t, const shared_ptr<_Tp>& __a) noexcept
  { return !__a; }

Smart Pointer转换为Dumb Pointers

一种形式是,提供隐式类型转换。但容易导致意料之外的转换。

标准库提供get()来返回原指针。

Smart Pointers 和 ”与继承有关的“ 类型转换

指向基类的智能指针和指向子类的智能指针之间没有继承关系,无法隐式转换。

但我们可以使用template来实现。

template<class newType>
operator SmartPtr<newType>() { //template function, 用于隐式类型转换操作符。
    return SmartPtr<newType> (rawPtr);
}

实际上,任何raw1指针可以转换成raw2指针的行为,对应的smart pointer也可以通过转成raw指针再实现转换。

Smart Pointer 与 Const

raw指针所指之物可以被const修饰,但智能指针不行。取而代之的是SmartPtr<const Obj>;

如同上面template实现转换,const的转换也可以同理实现。

或者使用继承和C Part of C++中的Union实现。

条款29:Reference counting

实现一个Reference Counting基类,方便子类实现copy-on-write功能。

将计数的操作全部封装在基类之中,子类只需判断是否可以共享即可。

注:RCPtr可使用库Smart Pointer实现。

class RCObject{
public:
    void addReference();
    void removeReference();
    void makeUnsharable();
    bool isShareable();
    bool isShared();

protected:
    RCObject();
    RCObject(const RCObject& rhs);
    RCObject& operator=(const RCObject& rhs);
    virtual ~RCObject() = 0;

private:
    int refCount;
    bool shareable;
};


class String{
public:
    String(const char *value= "");
    const char& operator[](int index) const;
    char& operator[](int index);

private:
    struct StringValue: public RCObject{
      char *data;
      StringValue(const char *initValue);
      StringValue(const StringValue& rhs);
      void init(const char *initValue);
      ~StringValue();
    };

    shared_ptr<StringValue> value;
};

同理,为了让已经实现好的类在不改变源代码的情况下,实现Reference Counting功能,只需再提供一层封装性。

需要实现一个间接智能指针,智能指针内含一个原生指针,该原生指针指向一个结构体,该结构体继承自RCObject,且内含有目的类的原生指针。

条款30:Proxy classes

使用Proxy可以用于区分operator []的读和写,使得copy-on-write技术更完善。但是,为了让Proxy Class尽可能地像原Class,需要重载大量操作符使得其像原Class(如&+=+-=等)。

同时,由于可能多一层隐式转换,使得原来能够实现的转换,在多一层的情况下,无法进行。

此外,Proxy Class需要承担一定的构造和析构成本。

条款31:让函数根据一个以上的对象类型来决定如何虚化

这条东西很多,主要关于”动态确定多个类型“,给出了一些解决方案。

  • 虚函数+RTTI
  • 只使用虚函数
  • 自行仿真虚函数表
  • ”继承“ + ”自行仿真的虚函数表格“

杂谈:Miscellany

条款32:在未来时态下发展程序

考虑程序后续的扩展性,如被派生可能导致的问题。

条款33:将非尾端类(non-leaf-classes)设计为抽象类(abstract classes

此条款里头讨论可能导致的问题,如

  • 子类之间的部分赋值。
  • 不同的子类之间的异型赋值。

挺抽象的,也没给出完美解决方案。

条款34:如何在同一个程序里头结合C++和C

C无重载,编译器不会对函数名字进行修改。

但C++有重载,会有name Mangling现象。使用 extern ”c“来避免。

简单守则如下

  • C++与C的兼容需要编译器产出兼容的目标文件(Object file);
  • 双方函数都声明为extern ”C“
  • 如果可能,尽量在C++中撰写main
  • deletenewfreemalloc需要配套。
  • C++ struct仅在无虚函数的情况下兼容C。

条款35:让自己习惯于C++语言

读者总结:Summary

通篇读下来,C++给我的感觉是,一个很硬核的语言,可以自己定制各种功能,事实上,C++也是由各类开发者发展完善的(如Boost社区)。

实现一个具有优良性质的设计,需要很深厚的经验和长远考虑等。


More Effective C++
https://messenger1th.github.io/2024/07/24/C++/More Effective C++/
作者
Epoch
发布于
2024年7月24日
许可协议