原文:C++ 惯用法: const 常量和字面量
作者:Breaker <breaker.zy_AT_gmail>
C++ 中 const 关键字、常量和字面量的惯用法
关键字:const, enum, const_cast, const_iterator, mutable, 左值, 字面量
本质和非本质上的常量
字面量 (literal),是指在 C++ 中直接书写的数字、字符和字符串,如 0x3f, '+', 1.23e-15L, "hello", L"world",具体语法参考 [CPP_LANG] 4.3.1 字符 4.4.1 整数 4.5.1 浮点数 5.2.2 字符串
左值 (l-value),是指想尽办法可以让其放到 = 左面的对象,这里的 = 是赋值而不是初始化,这里的对象可以是类类型和基本类型。想尽办法主要指强制转换和指针地址操作
本质上的常量,指非左值,字面量、enum 枚举值是非左值
字面量的 const 变量
是否能通过强制而修改,由编译优化和运行平台决定,这是 Win7 VC 2010 的情况:
字面量的 const 变量 | 是否能修改 |
---|---|
static 数字字面量 const | 不可修改 |
non-static local 数字字面量 const | 可修改 |
non-static local const char[] | 可修改 |
static const char[] | 不可修改 |
const char* 字符串字面量 | 不可修改 |
这些通过强制而修改字面量 const 变量的代码很难移植,并且不是正常的逻辑,所以认为字面量 const 变量是本质上的常量是合理的
const T& 引用
参考 [CPP_LANG] 5.5 引用
和字面量的 const 变量不同,字面量的 const T& 会引用一个临时变量
const int& g_C = 42; // 解释为: // int temp = 42; // const int& g_C = temp; // 所以强制是合理的 const_cast<int&>(g_C) = 43;类类型也是如此,并且 const T& 的初始值可以不是类型 T 的,会自动隐式转换,而这一点对于 non-const T& 不成立的,如:
// std::basic_string 中有 const char_type* 的转换 ctor void func_1(std::string& str); void func_2(const std::string& str); void test() { func_1("hello"); // Error, string& 不能接受 const char[] func_2("hello"); // OK, const string& 自动隐式转换 }非本质的常量
非本质的常量比本质的常量更容易理解和确定,通常是函数的 const 参数接受的 non-const 初始化,如:
char* strcpy(char* dest, const char* src); class Widget { public: void show() const; }; void func() { char str[] = "hello world"; char str2[64]; strcpy(str2, str); // strcpy 中 src 是非本质的 const Widget w; w.show(); // Widget::show 中 this 是非本质的 const* }非本质的常量的目的,是在一定执行区间内约束代码(不保证区间外是否 const),产生试图修改 const 的编译错误
常量求值
本质上的常量,可以在编译期求值,它们或化为指令立即数,或保存在目标文件 (.obj) 和可执行文件 (.exe/.dll) 的特殊位置,如 .rdata 区段
也因在编译期求值,本质上的常量可以作为非类型模板参数,只需注意指针类型模板参数只接受外部链接对象的地址,而字符串字面量是内部链接的,这时用外部链接的字符数组代替即可
非本质的常量,在运行时求值
有一些在运行时求值的非平凡情况,评估它们的常量本质性:
// strdup 早于 main 调用 const char* g_Str = strdup("hello"); // Widget ctor 早于 main 调用 const Widget g_Widget; void func() { const_cast<char*>(g_Str)[0] = 'A'; free(const_cast<char*>(g_Str)); const_cast<Widget&>(g_Widget).move(); // 设 Widget::move 是 non-const 方法 }常量存储
左值一定分配存储,因此可以取其地址
本质的常量(非左值)不一定分配存储,是否分配由具体情况、编译优化和运行平台决定,参考 [CPP_LANG] 5.4 常量
-
enum 不分配存储
-
字面量的 const 变量,如果不引用其地址,如创建指向它的指针或引用,则也不需要分配存储,但具体视编译优化而定
-
类的 static 整数 const,如果不引用其地址,则可以只用声明式而不用定义式,此时不需要分配存储;如果引用其地址,则需要定义式,并且分配存储
class Widget { static const int Num = 5; }; const int Widget::Num; // 引用 Num 地址时,需要定义式
const 对比 volatile
const 关键字的横向对比物是 volatile,对比两者可以更明白本质
-
和编译优化相关
volatile 是防止变量访问被激进优化的修饰字,volatile 变量的每次读写都会产生实际的内存访问指令,以防止因编译期不可知的因素(如并发线程的共享变量访问)而优化去除 volatile 变量的内存访问(如访问寄存器中的旧值)
本质 const 可能被优化存储,如存储到 ROM 中
优化一定和具体编译器与平台有关,这里 C++ 是定义一种适当的目的描述,而非实现细节,如 VC 2005+ 的 volatile 变量读写带有 Acquire/Release 语义以防止编译期指令 reorder,但仅在 IA64 上保证相同的运行时模型,x86 x64 上仍允许运行时 CPU reorder
本质 const 的存储也是平台相关的,所以上面表格中的 const_cast 很难移植
-
非本质的 const 和 volatile
非本质的 volatile 此称谓并不适当,权作和 const 对比,通常是函数的 volatile 参数接受的 non-volatile 初始化,如 InterlockedExchange(volatile LONG*, LONG) 中,它的目的和 const 参数相似,是在一定执行区间内约束代码(不保证区间外是否 volatile),防止那里的激进优化
-
用 const_cast 强制去除 const, volatile 修饰字
const_cast 只能去除非本质 const 的 const 修饰
-
指向 const/volatile 的指针和 const/volatile 指针,参考 MSDN: const and volatile Pointers
const T* pc; // 指针引用物是 const T* const cp; // 指针值是 const volatile T* vp; // 指针引用物是 volatile T* volatile pv; // 指针值是 volatile
const_iterator
一些 STL 容器如 vector 有 iterator 和 const_iterator 两种迭代器
vector<T>::const_iterator 是 const T*
const vector<T>::iterator 是 T* const
const 和 non-const 方法
class Widget { public: void move(int x, int y); // non-const 方法 // 解释为: // void move(Widget* this, int x, int y); void show() const; // const 方法 // 解释为: // void show(const Widget* this); }; void func() { const Widget cw; Widget w; w.move(42, 84); // OK, non-const 对象调用 non-const 方法 w.show(); // OK, non-const 对象调用 const 方法 cw.move(42, 84); // Error, const 对象调用 non-const 方法 cw.show(); // OK, const 对象调用 const 方法 };逻辑上的 const 方法
参考 [CPP_LANG] 10.2.7.1, 10.2.7.2 mutable
有时我们需要在 const 方法里修改对象的状态(成员变量),这时需要用 mutable 修饰那个被修改的成员变量
典型的惯用法:返回对象状态时,先检查其 cache 值,如果需要更新,则修改其值,如:
class Widget { public: // 这里的计算太简单,cache 效率可能比无 cache 差,仅作示例 // 合理的场景是有复杂计算过程的方法 int area() const { if (m_size_changed) { m_area = m_height * m_width; m_size_changed = false; } return m_area; } void move(int x, int y, int dx, int dy); // 设置 m_size_changed = true private: int m_height; int m_width; bool m_size_changed; mutable int m_area; };从 Widget 用户的角度看,Widget::area() 是具有 const 逻辑的
方法的 const 和 non-const 版本
一些 STL 容器如 vector 有同一个 operator 的 const 和 non-const 两个版本,如 vector<T> 实例化后有两个 operator[]:
class vector<T> { T& operator[](size_type p); const T& operator[](size_type p) const; };第一个 operator[] 可用来充当左值(准确的说是可变左值 modifiable l-value),因为所有的左值都是右值,所有它也可充当右值(注意 右值和非左值的区别)
那第二个版本 const 版本的 operator[] 岂不是多余
其实它和非本质 const 配合,用来约束代码,逻辑很合理:如果 vector<T> 是 const,那么每个元素都应该是 const,反之,假如元素能修改,就称不上是 const vector<T>,如:
void print(const vector<T>& vec) { for (vector<T>::size_type i = 0; i < vec.size(); i++) cout << vec[i] << '\n'; // 作用 1: 调用第二个版本的 operator[], 因为 vec 是 const* this vec[0] = T(42); // 作用 2: 不小心写错, 但 vec[0] 是 const T&, 编译出错从而保护 }分清两个位置 const 的作用:
作用 1, 返回值 const T&: 产生约束保护
作用 2, const 方法 (const* this): 产生重载规则,C++ 不能仅通过返回不同类型重载函数
也可以理解为,同一方法概念的两个版本:
non-const 版: set 式方法
const 版: get 式方法
转接 non-const 方法到 const 方法
虽然区分同一方法概念的两个版本,但大多数时候,const 和 non-const 版本的内部操作相同,仅在传入参数和返回值类型上不同
为了简化重复代码,可利用 static_cast/const_cast 将 non-const 方法转接到 const 方法,如 vector<T> 的 non-const operator 可如下转接:
T& vector<T>::operator[](size_type p) { return const_cast<T&>( // 转换 const T& 到 T&,因为事先知道 this 是非本质的 const*,所以强制是安全的 static_cast<const vector<T>&>(*this)[p]); // 转换 non-const* this 到 const* this,从而调用 const operator }反向的,将 const 转接到 non-const 上会违背 const 承诺,不要那么做
参考
[CPP_LANG] 《C++ 程序设计语言》特别版, Bjarne Stroustrup
[EFF_CPP] "Effective C++", 3Ed, Scott Meyers, 条款 02, 03