Effective C++阅读笔记 条款01~03 让自己习惯C++ 用const, enum, inline替换#define 尽量使用const

《Effective C++》的名声大家有目共睹,虽然现在用到C++大多只是用在算法题中,但C++的思想还是很容易迁移到其他语言上。只是……被网友封为神作的《Effective C++》的翻译简直不堪入目,还不如看原版呢……

1 让自己习惯C++

将C++看作4种次语言的联邦,在使用不同次语言时注意规则的转变:

  • C
  • 面向对象的C++
  • 模板C++
  • STL

这种思路的运用具体到传参方面:对C中的内建数据类型按值传参比按引用传参更加高效,但在面向对象的C++中由于有构造与析构函数的存在,const引用传参更加高效。

2 用const, enum, inline替换#define

避免使用#define ASPECT_RATIO 1.653,而是用const double AspectRatio = 1.653;
优点:

  1. 编译器根本看不到define的记号名称,在编译报错时一串数字可能令人感到困惑。
  2. 对浮点常量,预处理器的盲目替换将导致目标码(object code)中出现多份数据,使用const常量不会出现这种情况。

常量替换define时,需要注意:

  1. const与指针结合时到底修饰谁(下章详细介绍)
  2. 常量若是class专属变量,为了把常量的作用域限制在class内,必须让其成为class的成员,且为了此常量只有一个实体需要声明为static成员。
class GamePlayer{
public:
    static const int NumTurns = 5;
    int scores[NumTurns];
};

这里的NumTurns只是声明式而非定义式,而C++通常要求使用的任何东西都需要提供一个定义式。这里是一个例外:如果class专属常量又是static变量且为整数类型(int,bool,char),只要不取地址就不需要提供定义式。如果需要取地址,或者不取地址编译器仍报错(这不符合标准),需要提供一行定义在class外:const int GamePlayer::NumTurns;,此时不能再给初值。

#define并不能指定作用域,除非在某处被#undef,所以#define显然无法完成这里class内常量的任务。

class中的常量还可以通过enum hack实现:enum{i = 1, j = 2}; 这还可以阻止别人对其中的常量取地址,尽管优秀的编译器不会为“整数型const对象(const int)”设定另外的存储空间。注意enum hack技巧只适用于整数。

#define写出的宏通常也十分不可靠,就算你为宏的所有参数都加上括号以避免出现歧义,也很难阻止这种情况:

#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b)) //用a,b中的最大值调用f
int a = 5, b = 0;
CALL_WITH_MAX(++a, b);
CALL_WITH_MAX(++a, b+10);

由于前者a>b,++a的累加将被执行两次,而a+2<b+10,a的累加只会被累加一次,由此可见宏十分不可靠。
用template inline函数可以规避这一切,如下:

template<typename T>
inline void callWithMax(const T&a,  const T&b){
    f(a>b?a:b);
}

用这个函数不需要在函数本体中为参数加上括号(按引用传入时会提前计算),也不必担心在左++时参数被核算多次(左++根本没有进入函数,消除了不确定性),此外这是一个真正的函数,遵守作用域与访问规则,因此你完全可以写出private的class内inline template函数,传统宏却很难做到。

宏在#include,#ifdef和#ifndef的作用仍然难以完全消除,对我们程序员来说不在万不得已之时不要使用宏就好。

总结: 

  1. 对于单纯常量,最好以const对象或enums替代#define
  2. 对于形似函数的宏,最好改用inline函数替代#define

3 尽量使用const

const能修饰的对象:函数/指针/指针所指/变量
const与指针:const在*号左边,被指物为常量;const在*右边,指针自身是常量。
比如这两种写法都是说pw指向的对象是常量:const Widget pw;Widget const pw

const最具威力的用法是面对函数声明时的运用:可修饰函数返回值、参数、(如果是成员函数)函数自身。

  • 返回值为const能阻止无意识的错误,如const Rational operator* (const, const);(a \* b)=c;将会报错,尽管这符合人的直觉思维,但a*b的返回值为常数,c将无法赋值给它。书中总结道:只不过多打6个字符,你就可以省下无数恼人的错误。
  • const成员函数:用const标记成员函数可以得知哪个函数可以改动对象内容而哪个不行;除此之外,这还使得操作const对象成为可能。
  • 需要注意,两个函数如果只是常量性不同(是否被声明为const),是可以被重载的。例如以下用string封装的TextBlock:
    class TextBlock{
    public:
        const char&operator[](std::size_t position) const{
            return text[position];
        }
        char&operator[](std::size_t position){
            return text[position];
        }
    private:
        std::string text;
    };

如果不声明为常对象,载入的函数为后者,否则为前者。需要注意这类[]返回值均为引用类型,否则会因为C++的按值返回而达不到修改对象的目的:TextBlock tb("Hello"); tb[0] = '1'; //返回值必须为引用

编译器是如何处理const成员函数的呢?这其中有两个流行概念:bitwise constness与logical constness。

bitwise constness

前者很好理解,字面意思就是不修改对象内的任何一个bit,这也是C++对常量性的定义,因此常成员函数不可以更改对象内任意非静态成员变量。也就是说,这样的行为是允许的。

int b = 1;
class TextBlock{
public:
    const char&operator[](std::size_t position) const{
        b = 2; // 在常成员函数中修改全局变量是允许的
        return text[position];
    }

但bitwize const有时却难以发现不够const的行为,例如修改了指针所指物的成员函数理应不算是const成员函数,但如果只有指针属于对象而指针所指物不属于对象,这样的行为不会触发编译错误。以这段代码为例:

    class TextBlock{
    public:
        char&operator[](std::size_t position) const{
            return text[position];
        }
    private:
        char* pText;//与上端代码的不同之处在于此处数据被存储为char*
    };

这里的operator[]函数并不修改pText,这属于bitwise const,但:

const CTextBlock cctb("Hello');
char* pc = &cctb[0];
*pc = 'j';

这就引发了意料之外的事:你创建了常量对象并初始化,且只对它应用const成员函数,然而它的值还是能通过改变指针所指物来改变。

logical constness

logical constness主张const成员函数可以修改对象内部的某些bits,但只有在“客户端侦测不出的情况下才可如此”。这句话可能很难理解,事实上这句话是为了引出mutable关键字,如:

class CTextBlock{
public:
    std::size_t length() const;
private:
    char* pText;
    std::size_t textLength;
    bool lengthIsValid;
 };
  std::size_t CTextBlock::length() const{
    if(!lengthIsValid){
        textLength = std::strlen(pText);
        lengthISValid = true;
    }
 }

显然CTextBlock::length()函数不符合bitwise原则,但只要做如此变动:

mutable std::size_t textLength;
mutable bool lengthIsValid;

mutable会释放掉非静态成员变量的bitwise constness约束,使它们可以被const成员函数改动,以实现logical constness。

在const和non-const成员函数中避免重复

上面的例子中你可能会发现,同样的逻辑都被应用到了const和non-const函数中,如果无脑复制将导致代码冗长,也许我们可以这样:按业务逻辑写好const成员函数,在处理非const对象时只需调用按规则调用const成员函数即可。如:

    class TextBlock{
    public:
        char&operator[](std::size_t position) const{
            ....// 一大堆业务逻辑
            return text[position];
        }
        char&operator[](std::size_t position){
            return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
        }
    private:
        char* pText;
    };

在这个non-const函数中实现了两个转型:

  • 首先我们明确non-const应该调用它的兄弟const而非自身,否则将陷入死循环。完成这一步需要将*this转换为const TextBlock&。
  • 其次由于这是non-const函数,所以从返回值中移除const。

第一次转型是安全的,使用static_cast,而第二次只能使用const_cast别无他法(或许有一些C-style的方法)。

请不要耍聪明反向操作:用const去调用non-const的逻辑,因为non-const并不承诺不改动对象。

总结:

  1. const可被施加于任何作用域内的对象,函数参数,函数返回类型,成员函数本体。
  2. 编译器强制实行bitwise const,尽管有时它一点也不const。
  3. 当成员函数的const和non-const类型逻辑一致时,令non-const版本调用const版本可避免代码重复。
Last modification:August 14th, 2019 at 11:01 am
If you think my article is useful to you, please feel free to appreciate

Leave a Comment