Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1075198
  • 博文数量: 77
  • 博客积分: 11498
  • 博客等级: 上将
  • 技术积分: 1840
  • 用 户 组: 普通用户
  • 注册时间: 2006-05-04 11:10
文章分类

全部博文(77)

文章存档

2011年(1)

2010年(16)

2009年(5)

2008年(55)

分类: C/C++

2008-05-18 16:27:27


    《C++ Coding Standards》读书笔记。
    作者:tyc611.cublog.cn,2008-05-18
 
1.二元操作符

    一般而言,对于二元操作符@(如+、-、*、=等),应该定义有两种形式:operator @和operator @=,对应于两种形式的表达式:a @ b 和 a @= b。通常,都是由operator @=来实现operator @,但在方便实现的前提下也可以反过来进行(对于一些算法这样做可以提高效率)。基本接口及实现如下:
    T& T::operator @= (const T&) {
        // …实现代码
        return *this;
    }
    T operator @ (const T &lhs, const T &rhs) {
        T tmp(lhs);
        return tmp @= rhs;
    }

    对于上面operator @的实现,可以考虑通过值传递来消除函数体中的临时变量tmp,这可能有助于编译器优化(例如,当传入的参数本身是临时变量时,可以减少一次复制构造)。新的接口及实现如下:
    T operator @ (T lhs, const T &rhs) {
        return lhs @= rhs;
    }

    根据“优先编写非成员非友元函数”的原则,在可能的前提下,可以考虑将operator @=也实现为非成员函数。新的接口及实现如下:
    T& operator @= (T &lhs, const T &rhs) {
        // …实现代码
        return lhs;
    }
    T operator @ (T lhs, const T &rhs) {
        Return lhs @= rhs;
    }

    非成员函数形式的二元操作符可以接受左操作数(当然也包括右操作数)的隐式转换。如果要避免此过程中临时变量的产生,可以考虑进行重载(应尽可能地考虑重载)。

    当将自定义操作符实现为非成员函数时,一定要将该操作符放入其操作数类型定义所在的同一名字空间中,这有利于ADL(Argument Dependent Lookup)的顺利进行,同时也方便调用者使用。

    源自:C++ Coding Standards, Item 27
    更多参考:
    More Effective C++ 1996 Item 22
    Exceptional C++ 2000 Item 20

2.赋值操作符

    赋值操作符一般有如下两种形式:
    T& operator = (const T&);     // 传统形式
    T& operator = (T);           // 需要参数副本

    如果需要在operator=函数体内生成参数的副本(比如swap惯用法),则可以考虑第二种形式,这有利于编译器进行优化(例如,当传入的参数本身是临时变量时,可以减少一次复制构造)。

    不要将赋值操作符设为虚函数;不要返回const T&(STL容器要求operator返回T&);要显式地调用基类赋值操作符;返回*this;需要保证自赋值安全,一种方法是显式检查,另一种方法是使用swap惯用法。

3.一个笑话

    关于C++有一个古老的笑话,这种语言之所以称为C++而非++C,是因为它虽然已经进行了改进(递增),但是很多人还是把它当C(原值)使用。

    An ancient joke about C++ was that the language is called C++ and not ++C because the language is improved (incremented), but many people still use it as C (the previous value).

4.名字空间

    绝对不要在#include之前编写using声明或者using指令,不要在头文件中编写名字空间级的using声明或者using指令。

    使用using声明时,所引入的名字是在遇到using声明瞬间所见到的名字空间中的所有该名字实体。

5.分清要编写的是哪种类

    不同种类的类适用于不同用途,因此遵循着不同的规则。关于类设计有一个重要的原则:要添加行为,优先考虑非成员函数(而不是成员函数);要添加状态,优先考虑组合(而不是继承);要避免从具体类中继承。

5.1.值类
    值类模仿的是内置类型,它应该:
    1) 有一个公有析构函数,复制构造函数和带有值语义的赋值。
    2) 没有虚函数。
    3) 用作具体类,而不是基类;

5.2.基类

    基类是类层次结构的构成要素,它应该:
    1) 有一个公有且虚的,或者保护且非虚的析构函数,和一个非公有复制构造函数和赋值操作符。
    2) 通过虚函数建立接口。

5.3.traits类

    traits类是携带类型信息的模板,它应该:
    1) 只包含typedef和静态函数。没有可修改的状态或者虚函数。
    2) 通常不实例化(其构造一般是被禁止的)。

5.4.策略类

    策略类(通常是模板)是具有可插拔行为的片段,它应该:
    1) 可能有也可能没有状态或者虚函数。
    2) 通常不独立实例化,只作为基类或者成员。

5.5.异常类

    异常类提供了不寻常的值与引用语义的混合:通过值抛出,但应该通过引用捕获。一个异常类应该:
    1) 有一个公有析构函数和不会失败的构造函数(特别是一个不会失败的复制构造函数,从异常的复制构造函数中抛出异常将使程序异常中止)。
    2) 有虚函数,经常实现克隆和访问。
    3) 从std::exception虚拟派生更好。

6.提供抽象接口

    抽象接口是完全由(纯)虚函数构成的抽象类,没有状态(成员数据),通常也没有成员函数实现。

    依赖倒置原理(Dependency Inversion Principle, DIP):
    1) 高层模块不应该依赖于低层模块。相反,两者都应该依赖于抽象。
    2) 抽象不应该依赖于细节。相反,细节应该依赖于抽象。

    遵守DIP意味着类层次结构应该以抽象类而不是具体类为根。换句话说,策略应该上推,实现应该下放。

    二次机会定律(Law of Second Chances):最重要的是要保证接口正确,其它所有东西在以后都可以修改。但如果接口弄错了,可能再也不允许修改了。

7.公有继承

    公有继承是为了被(已经多态地使用了基类对象的已有代码)重用的,而不是重用(基类中的已有)代码。公有继承的目的是为了实现可替换性。

    Liskov替换原则(Liskov Substitution Principle):公有继承建模的是“is-a”关系(更精确描述为“works-like-a”关系)。

    为了保持可替换性,在改写虚函数时,不要改变虚函数的默认参数,应该显式地将改写函数重新声明为virtual。

8.非虚拟接口(NVI)

    公有虚函数本质上有两种不同且相互竞争的职责:
    1) 它指定了接口:作为公有函数,它是类向外界提供的接口的一部分;
    2) 它指定了实现细节:作为虚函数,它为派生类替换函数的基类实现(如果有的话)提供了一个钩子,它是一个自定义点。

    非虚拟函数接口(Nonvirtual Interface, NVI)模式的目的就是为了分离公有虚函数的这两种职责:将公有函数设为非虚的,将虚函数设为私有的(或者保护的,如果派生类需要调用基类版本的话)。注意,NVI与Template Method模式很相似,但是动机和意图不同(后者主要是为了提供了一个统一的算法流程并可定制其中某些行为)。

    由于NVI需要在公有接口内部调用虚函数,因此NVI对析构函数不适用。

9.优先编写非成员非友元函数

    可按如下方法确定函数是成员、友元还是非成员非友元函数:
    1) 如果函数是操作符=、->、[]、()之一,则必须是成员(语言规定);
    2) 否则,如果二元操作符函数需要其它类型的左操作数(例如,操作符>>、<<),或者需要对其左参进行强制转换,或者能够用类的公有接口单独实现,则指定为非成员函数(前两情况可能需要实现为友元函数)。
    3) 否则,将其指定为成员函数。

10.为new提供所有标准形式

    如果类提供了专门的new,则应该提供所有三种标准形式:普通(plain)、就地(in-place)和不抛出(nothrow)。

    C++中,如果在某个作用域中定义了一个名字,那么该名字将会隐藏所有外围作用域(比如基类、外围名字空间)中的相同名字,而重载永远不会跨越作用域。

    而在STL的容器中,in-place new被广泛使用,因此应该总是避免隐藏in-place new。

11.以同样的顺序定义和初始化成员变量

    编译器总是按照成员变量声明顺序来执行初始化,因此我们在初始化列表中进行初始化时也应该按照变量声明的顺序进行。同时,也尽量不要让成员变量的初始化依赖于其它成员变量。

12.避免在构造函数和析构函数中调用虚函数

    避免在构造函数和析构函数中直接或者间接调用虚函数。

    在构造函数或者析构函数中调用虚函数,该调用是静态绑定,调用的是该类的虚函数版本,而不会调用到派生类中的版本。如果从构造函数或者析构函数中直接或者间接调用未实现的纯虚函数,会导致未定义行为。

    当确实需要在构造完对象后调用虚函数,可以采用后构造技术:
    1) 强制用户在构造对象后调用类似于Initialize的函数进行初始化。
    2) 使用一个内部标识来标识是否已经初始化,并在第一次调用成员函数时进行初始化。
    3) 使用工厂方法

    使用工厂方法的例子如下:
    class B {
    protected:
        B() { /* .. */ }
        virtual void PostInitialize() { /* … */ }
    public:
        template
        static shared_ptr Create() {
            shared_ptr p(new T);
            p->PostInitialize();
            return p;
    }
    };
    class D: public B { /* … */ }
    shared_ptr p = D::Create();
   
13.析构函数

    将基类析构函数设为公有且虚的,或者保护且非虚的。如果允许通过基类的指针执行删除操作,则析构函数必须是公有且虚的;否则,就应该是保护且非虚的。

    由于默认生成的析构函数是公有且非虚的,所以应该总是为基类编写析构函数。

    策略类经常被作用基类,是出于方便考虑的,而不是出于多态行为考虑的。因此,它们的析构函数通常应为保护且非虚拟的。

    析构函数不允许失败(释放和交换也有此要求)。析构函数不能抛出异常,否则terminate函数将被调用。标准库禁止所有与之一起使用的析构函数抛出异常,标准库中定义的析构操作都不会抛出异常。

14.多态复制

    如果在基类中需要进行多态复制,那么应当禁止复制构造函数和复制赋值操作符,提供虚拟的Clone成员函数。Clone函数可以考虑NVI模式。例如:
    class Base {
    public:
        Base* Clone() const {
            Base *p = DoClone();
            assert(typeid(*p) == typeid(*this) && “DoClone incorrectly overridden”);
            return p;
        }
    protected:
        Base(const Base&);
    private:
        virtual Base* DoClone() const = 0;
    };

    此时,派生类只需要改写DoClone函数即可。

15.切片

    如果允许切片,但又不想调用者不小心地意外切片,那么可以将基类的复制构造函数设为explicit。此时,不能够通过值传递方式来传递基类对象,从而避免意外切片的发生,使用者只能显式地调用复制构造函数进行切片。

16.模块

    不要允许异常跨越模块或者子系统边界进行传播。确保所有模块在内部一致地使用一种错误处理策略,而在其接口中也一致地使用另一种错误处理策略(Item 62)。

    不要让类型出现在模块的外部接口中,不应该在模块外部接口中使用标准库类型(除非使用该模块的所有其他模块使用相同的标准库且相同的编译选项进行编译)。使用的抽象层次越低,可移植性就越好,但复杂性也越高。

17.类模板

    不要将虚函数放入类模板中,否则将导致所有虚函数每次都实例化,进而引起编译后目标代码大大增加(且不会被调用)。

    为了避免无意地提供自定义点,应该:

    将模板内部使用的任何辅助函数都放入其自己的内嵌名字空间,并用显式的限定调用它们以禁用ADL(也可以通过将函数名放入括号内达到此目的)。例如:
    template
    void Example(T t)
    {
        ExampleHelper::bar(t);    // 禁用ADL,bar不是自定义点
        (bar)(t);                // 另一种方式
    }

    要避免依赖于依赖名,尽量避免依赖于“两阶段查找”规则(很多编译器未能遵守此标准)。在引用依赖基类的任何成员时,应该总是用基类名或者this指针显式地进行限定。例如:
    template
    class C: X
    {
        typename X::SomeType s;       // 使用基类的内嵌类型或者typedef
    public:
        void f() {
            X::bar();     // 调用基类成员函数
            this->bar();      // 另一种方式
        }
    };

18.函数模板

    不要特化函数模板,相反,应当使用函数重载(函数模板重载和普通函数重载)来达到目的。原因在于,函数模板特化不会参与函数重载解析(但不表示不会被调用),这将与特化函数模板的初衷相违背(Exceptional C++ Style, Item 7, 2004)。

    函数模板不能够偏特化,但可以使用重载达到相同的效果(就像类模板不能重载却可以使用偏特化达到目的一样)。

    如果要编写新的函数模板,应该使用类模板来实现函数模板。这样就可以进行偏特化和显式特化,避免了函数模板无法被部分特化的限制以及函数模板特化无法参与重载的情况。

19.断言

    在模块内部尽量使用断言(如assert)来保证各种假设,但要确保断言不会产生任何副作用,毕竟断言只用于Debug版本。

    在使用断言时,通过在断言表达式中加入“&& “Information message””来提供更丰富的信息,同时也可以取代相应的注释。

    不要使用断言报告运行时错误!

20.异常

    不要在函数中编写异常规范,除非不得已而为之(如其它无法修改的代码已经使用了异常规范)。

    通过值抛出异常,通过引用(通常是const引用)捕获异常。当重新抛出异常时,应该优先使用throw,避免使用throw e,这样才能够保证重新抛出对象的多态性。

    在模块内部,应该使用异常而不是错误码来报告错误,性能通常不是异常处理的缺点。

21.STL容器

    如果插入和删除元素需要事务性语义,或者需要尽量减少迭代器失效,则应该优先使用基于节点的容器(例如list、set和map)。

    如果查找速度是关键的考虑因素,则优先选择基于散列的容器,其次考虑使用有序的vector(二分查找),再后是set或map。

    默认情况下,应该使用vector。vector是标准库容器中惟一能够保证如下性质的容器:
    1) 保证具有所有容器中最低的空间开销(每个对象只需0个字节)。
    2) 保证具有所有容器中对元素进行存取的速度最快。
    3) 保证容器中相邻对象在内存中也相邻,这是其它标准容器都无法保证的。
    4) 保证具有与C语言兼容的内存布局,这与其它标准容器不同,从而与C语言的API协同工作。
    5) 保证具有所有容器中最灵活的迭代器(随机访问迭代器)。虽然deque也提供随机访问迭代器,但要慢很多。
    6) 几乎肯定具有最快的迭代器(指针,或者性能相当的类),比其他所有容器的迭代器都快。
    7) 将vector用于小型列表几乎都是优于使用list。

    不要将迭代器当作指针,特别是vector::iterator。例如,要获取vector::iterator it所引用元素的地址(以传递给C语言API),应该使用&*it。由于vector的存储区总是连续的,因此可以使用&*v.begin()、&v[0]、&v.front()来获取v的第一个元素的指针。当要获取第n个元素的指针时,应该先做运算再取地址(例如,&v[n]),而不先获取头部指针然后进行指针运算(例如,(&v[0])[n])。这是因为,前一种情况下,能够给带检查的标准库实现一个机会,验证有没有越界访问v的元素。

    shrink-to-fit惯用法:container(c).swap?;

    erase-remove惯用法:c.erase(remove(c.begin(), c.end(), value), c.end());(对于有remove或者remove_if成员的容器,应该尽量使用成员版本)

22.STL算法

    对于有序范围,应该将 p = equal_range(first, last, value); distance(p.first, p.second); 当作 count(first, last, value)的更快版本使用。但是对于有count成员版本的容器,应该使用成员函数版本。

    应该向算法传递函数对象,而非函数。函数对象的适配性好,并且通常产生的代码一般比函数要快。另外,关联容器的比较器必须是函数对象。

23.类型安全

    不要使用C风格的强制转换,其可能会不加提示地插入有害的reinterpret_cast。

    在不相关的指针类型之间强制转换时,应该间接通过void*使用static_cast进行转换(能够自动对齐),而不要使用reinterpret_cast。
阅读(1554) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~