什么是 C++ 中的 const

C++中的`const`是一个非常容易混淆的概念,我认为这主要是因为`const`的设计不够完善导致的,尽管目前C++11中新加入了`constexpr`关键词,解决了一部分问题,但是还有很多本质上的问题没有得到解决,甚至很多人都没有认识到这些问题的本质。我在这里解释一下我对C++中`const`的理解,并且试图分析其所解决的本质问题,对于部分问题,给出一些其他方案。

C++中的`const`的3种用法

C++中的`const`有3种用法:

  1. 修饰一个变量(类型),表示这个变量(类型)不可修改

  2. 修饰一个变量(类型),表示这是一个常量

  3. 修饰一个成员函数,表示这个成员函数不会修改类的内部状态

这三种用法中,第2种已经被新的关键字`constexpr`所取代,而第1种和第2种用法或多或少有些问题。

`const`表示变量(类型)不可修改

代码实例:

const int a = f();
cout << a << endl; // OK
a = 5; // Compile Error! `a` is readonly.

其实大家在使用这种方式的时候,只是想表明被修饰的*变量*a`是只读的。但是C++在这里有一个问题,就是`const`修饰的并不是变量,而是*变量的类型*。这里变量`a`的类型是`int const,这个类型和`int`是不兼容的,这经常导致一些非常傻逼的情况:

  1. 将函数的返回值类型设置为`const`类型的

  2. 将函数的参数类型设置为`const`类型的,到后来发现有一个很深的依赖函数需要修改这个值

第1种情况在修饰栈上对象的时候就是一种纯傻逼行为,因为函数返回的时候一拷贝就能去掉`const`修饰符,相当于什么都没干;在修饰指针(含引用)类型的时候,则需要更多的考虑以防出现第2种情况这样缺乏远见的行为。

C++之所以这么做,就是因为对象有可能是在栈上的,也有可能是堆上的,而这两种情况对于用户是不透明的,用户必须知道这个对象是在哪儿的,并且在代码中反映出这种不同,这对于C++程序员而言是相当痛苦的。当一个对象是在堆上的时候,尽管我们不允许修改指向这个对象实例的指针,但是仍然有可能修改其所指向的对象实例的内容。为了防止这种情况发生,C++设计的时候让`const`修饰类型,这样我们使用只读类型的时候,无论是在栈上还是堆上都不能修改对象实例的内容了。

`const`修饰成员函数

示例代码:

class A
{
private:
    int m_id;
public:
    int get_id(void) const;
    void set_id(int id);
}

这个问题和上一个问题是高度相关的,正因为`const`在表示只读时修饰的是类型,所以就需要标记出哪些成员函数是只读的,这些成员函数可以在只读类型中继续使用,而其他成员函数不可以。

说到这里,我们突然发现,const`的这种用法竟然是在定义一个新的类型——`X`类型的只读类型`X const

这也是为什么很多程序员都不愿意在成员函数上标记`const`。

C#中的`readonly`关键字

C#中有两个关键字,readonly`和`const。前者用于修饰一个变量,表示该变量只读。后者也是用于修饰一个变量,表示该变量是编译时期常量。我认为这是一种比较好的设计,因为这种设计中,两个关键字分别表示两种完全不同的语义,并且`readonly`的语义符合人们修饰一个变量而非其类型的预期。

`readonly`只能控制栈上(含寄存器)对象不被修改,比如`int`类型的变量,或者对象引用本身。如果想要控制堆上变量不被修改,则需要针对这一类型定义一种新的只读接口,使用这一只读接口类型才能控制堆上变量本身不被修改。

示例代码

interface AReadOnly {
    int GetId();
}
class A {
    private int m_id;
    public GetId();
    public SetId(int id);
}

AReadOnly a_const = new A();