条款03:尽可能使用const

1. 总结

  • const可用于任何作用域内的对象、函数参数、函数返回值、成员函数自身,将这些内容声明为const可帮助编译器侦测出错误用法
  • 对于const成员函数,C++编译器强制要求bitwise constness,但在编写程序时应该使用"概念上的常量性"
  • const成员函数可以修改被mutable关键字修饰的non-static成员变量
  • 当const和non-const成员函数有着实质等价的函数体时,令non-const版本调用const版本可避免代码重复,但绝对不能反过来调用

2. const对象

关键字const可以用于以下对象。

  • 在class外部修饰global或namespace作用域中的常量
  • 修饰区块作用域(block scope)中被声明为static的对象
  • 修饰class内部的static和non-static成员变量
  • 面对指针,根据“左数右指”口诀,可以指出指针自身、指针所指数据,或两者都是(或都不是)const

STL迭代器以指针为根据塑模出来,所以STL迭代器的作用就像个T *指针。

  • 声明迭代器为const就像声明指针为const一样,即声明一个T *const指针,迭代器本身不可修改,但其指向的数据可以被修改
  • 如果希望迭代器所指向的数据不可修改,即声明一个const T *指针,则需要使用const_iterator
std::vector<int> vec;

const std::vector<int>::iterator iter = vec.begin();        //iter相当于T *const指针
*iter = 10;                                                 //没问题
++iter;                                                     //错误,iter不可修改

std::vector<int>::const_iterator const_iter = vec.begin();  //const_iter相当于const T *指针
*const_iter = 10;                                           //错误,*const_iter不可修改
++const_iter;                                               //没问题

3. const函数返回值和函数参数

const最具威力的用法是面对函数声明时的应用。在一个函数声明中,const可以和函数参数、函数返回值、函数自身(如果是成员函数)产生关联。
const用于函数参数只需记住一条原则:除非函数体中需要改动参数,否则就将它们声明为const
const用于函数返回值,往往可以降低因使用错误而造成的意外,同时又不至于放弃安全性和高效性,举个例子,看下面有理数类operator *的声明。

class Rational { ... };
const Rational operator * (const Rational &lhs, const Rational &rhs);
Rational a, b, c;

(a * b) = c;    //有意错误,对两个数的乘积进行赋值,就好比1 = 2一样
if (a * b = c)  //无意错误,将==漏写为=
  • 如果a、b、c都是内置类型,上述代码直接就是不合法
  • 而对于重载了operator *的class,由于operator =的存在,如果不对返回值使用const,编译器就不会报错
  • 然而,一个良好的用户自定义类型的特征是避免它们无端地与内置类型不兼容(见条款18)
  • 因此,将operator *的返回值声明为const,就可以让编译器检测出这种错误用法

4. const成员函数

const成员函数的重要性

将const用于成员函数的目的,是为了确认该成员函数可作用于const对象身上。const成员函数之所以重要,基于两个理由。

  • 它们使class接口比较容易被理解,可以很明显的得知哪些函数可以改动对象而哪些函数不能
  • 它们使操作const对象成为可能

第2条对于编写高效代码是个关键,因为如条款20所述,改善C++程序效率的一个根本方法是以const引用的方式传递对象。
const引用的可能是const对象,而const对象只能调用const成员函数。
所以此技术可行的前提是,有const成员函数可用来处理取得的const对象;否则,就算能将const对象传进来,也没有办法去处理它。

C++有一个重要特性:两个成员函数如果只是常量性不同,则可以构成重载,即使它们的参数类型、参数个数、参数顺序都完全一致。
基于这个特性,再结合上面提到的高效编码技巧,就可以得出如下所示的接口设计。

class TextBlock
{
private:
    std::string text;
public:
    //用于const对象,由于const对象内容不允许修改,因此返回值也加了const
    const char &operator [] (std::size_t postion) const
    {
        return text[postion];
    }

    //用于non-const对象
    char &operator [] (std::size_t postion)
    {
        return text[postion];
    }
};

void print(const TextBlock &text)
{
    std::cout << text[0];
}

TextBlock text;
const TextBlock const_text;

print(text);        //调用char &operator [] ()
print(const_text);  //调用const char &operator [] () const

bitwise constness

C++编译器要求const成员函数不能更改对象内的任何non-static成员变量,简单地说就是const成员函数中不能出现对non-static成员变量的赋值操作。
这种要求实质上是不能更改对象内的任何一个bit,因此叫做bitwise constness。
不幸的是,许多const成员函数虽然不完全具备const性质,但却能通过C++编译器的bitwise检验,更具体地说,就是:

  • "概念上的常量性"来看,一个更改了指针指向数据的成员函数不能算是const成员函数
  • 但如果只有指针隶属于对象,那么称此函数为bitwise constness不会引发编译器异议
class TextBlock
{
private:
    char *pText;
public:
    char &operator [] (std::size_t postion) const
    {
        return pText[postion];
    }
};

const TextBlock text("Hello");  //声明一个const对象
char *pc = &text[0];            //调用const char &operator []取得一个指针,指向text的数据
*pc = 'J';                      //通过pc指针将text的数据改为了"Jello"

上面这个class将operator []声明为const成员函数,但却返回了一个reference指向对象内部数据,这种做法是错误的,条款28对此有深刻讨论,我们暂时先忽略它。
从编译器bitwise constness的角度看,上述代码不存在任何问题,但你终究还是改变了const对象的值,这种情况导出所谓的logical constness。

logical constness

logical constness指的是,const成员函数可以修改它所处理对象内的某些bits,但前提是用户察觉不到这种修改。
要想在const成员函数中修改non-static成员变量,需要对这些成员变量使用mutable关键字,mutable可以去除non-static成员变量的bitwise constness约束。

class CTextBlock
{
private:
    char *pText;
    mutable std::size_t textLength;  //最近一次计算的文本长度
    mutable bool lengthIsValid;      //目前的长度是否有效
public:
    std::size_t length() const;
};

std::size_t CTextBlock::length() const
{
    if (!lengthIsValid)
    {
        textLength = std::strlen(pText);
        lengthIsValid = true;
    }

    return textLength;
}

length()的实现当然不是bitwise constness,因为textLength和lengthIsValid都可能被修改,但这两个成员变量被修改对于const CTextBlock对象是可以接受的

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

现在我们对class TextBlock做一些修改,假设operator []不单只是返回一个reference指向某字符,还执行边界检查、日志数据访问、数据完整性检验等工作。

class TextBlock
{
private:
    std::string text;
public:
    //用于const对象,由于const对象内容不允许修改,因此返回值也加了const
    const char &operator [] (std::size_t postion) const
    {
        ...         //边界检查
        ...         //日志数据访问
        ...         //数据完整性检验
        return text[postion];
    }

    //用于non-const对象
    char &operator [] (std::size_t postion)
    {
        ...         //边界检查
        ...         //日志数据访问
        ...         //数据完整性检验
        return text[postion];
    }
};

operator[]的const和non-const版本中的代码重复,可能会随着编译时间、持续维护、代码膨胀等因素而成为令人头痛的问题。
将重复代码封装到一个private函数中,并分别在两个函数中调用它,不失为一个解决该问题的好办法,但依然存在代码重复,如函数调用、return语句。
真正最好的办法是:先实现operator []的const版本,然后在non-const版本中调用它。如下示例代码所示,这种方法有两个技术要点。

  • 先使用static_cast为*this添加const属性
  • 接下来调用const版本成员函数,并使用const_cast去除返回值中的const,最后作为non-const函数的返回值返回
class TextBlock
{
private:
    std::string text;
public:
    //用于const对象,由于const对象内容不允许修改,因此返回值也加了const
    const char &operator [] (std::size_t postion) const
    {
        ...         //边界检查
        ...         //日志数据访问
        ...         //数据完整性检验
        return text[postion];
    }

    //用于non-const对象
    char &operator [] (std::size_t postion)
    {
        const TextBlock &const_this = static_cast<const TextBlock &>(*this);  //将自身从TextBlock &转换为const TextBlock &
        return const_cast<char &>(const_this[postion]);                       //调用const版本的operator [],并去除返回值中的const属性,然后返回
    }
};

注意,千万不要令const版本调用ono-const版本来避免代码重复,因为const版本调用non-const版本的唯一方法是去除自身的const属性,这绝对不是个好事情。

posted @ 2020-01-12 16:03  原野追逐  阅读(511)  评论(0编辑  收藏  举报