const
真理大讨论之mutable
接着上篇文章说,当我们还在语义const
和语法const
的实现效果不一致中苦苦挣扎的时候,我们实际需要特殊声明变量在封装内部为一种“可变的”状态,但对外提供表现出来的应该是const
属性。mutable
是在C++中被讨论相对较少的一个关键字。但是它却十分有用,而且在你想写出const
正确的代码时几乎不可避免。
我们的核心观点在于,在外部表现为const
的变量在内部转变为非const
这一行为应该尽量不被外部所感知。然而如果我们定义了一个函数为const
后,编译器不会允许我们再改变任何一个成员变量,即便在外部它表现出const
属性,这就是最核心的矛盾。
接着上篇文章中Mesh类的getVolume()
例子,我们继续深入一下:
Mutex
在上篇文章中,我们使用的Mesh类并不是线程安全的。所以在一个多线程的应用中,一个Mesh
类可能会被多个线程共享,我们可能会用互斥锁Mutex来保证数据的线程安全。像下边这样:
class Mesh {
vector<Vertex> vertices; //模型顶点的集合
std::mutex mtx;
double volume; //计算出来的体积
bool volumeCalculated; //当前体积是否已计算的标识
public:
Mesh(std::vector<Vertex> vxs = {}): volume{0}, volumeCalculated{false}, vertices(std::move(vxs)) {}
double getVolume() const {
std::scoped_lock lock{mtx}; //Error
if(volumeCalculated) {
return volume;
}
volume = geometry::calculateVolume(vertices); //Error
volumeCalculated = true; //Error
return volume;
}
void addVertex(Vertex const& v) {
std::scoped_lock lock{mtx};
vertices.push_back(v);
volumeCalculated = false;
}
//...
};
这里,编译器会在getVolume()
函数中报错,因为我们试图向一个scoped_lock
中传递了一个const mutex
并且试图调用mutex::lock
。但是,我们并不能lock
一个const mutex
。我想重申,这里我之所以想方设法地把getVolume()
设为const
类型,是因为在函数的调用者来看,这就应是一个返回当前Mesh化后模型体积的函数。一个getter函数理应是const
类型,但它的实现细节并不应表现出来。
(如果你奇怪上边模板实例化参数去哪了,在C++17中我们已经有了类模板参数参数推断(class template argument deduction))
mutable
关键字mutable
就是专门用来解决这个问题的——语义const
和语法const
不一致的问题。当一个成员变量被声明为mutable
时,它显示地表明“这个变量可能在一个const
语义环境中被更改”。有了mutable
上边两个问题的解决方案就会变成下边这样:
class Mesh {
vector<Vertex> vertices; //模型顶点的集合
mutable std::mutex mtx;
mutable double volume; //计算出来的体积
mutable bool volumeCalculated; //当前体积是否已计算的标识
public:
Mesh(std::vector<Vertex> vxs = {}): volume{0}, volumeCalculated{false}, vertices(std::move(vxs)) {}
double getVolume() const {
std::scoped_lock lock{mtx}; //OK
if(volumeCalculated) {
return volume;
}
volume = geometry::calculateVolume(vertices); //OK
volumeCalculated = true; //OK
return volume;
}
void addVertex(Vertex const& v) {
std::scoped_lock lock{mtx};
vertices.push_back(v);
volumeCalculated = false;
}
//...
};
mutable
可以放在任何不是引用或没被显式声明为const
的成员变量前。
mutable
lambda表达式
mutable
的另一个用途是给lambda表达式准备的。通常来说,一个闭包(lambda表达式)的调用时const
的行为,因此,lambda无法修改任何按值捕获(captured by value)的成员:
int main() {
int i = 2;
auto ok = [&i](){ ++i; }; //OK, i是按引用捕获
auto err = [i](){ ++i; }; //Error: 试图修改按值捕获的变量i
auto err2 = [x{22}](){ ++x; }; //Error: 试图修改内部变量x
}
这里,对lambda表达式加上关键字mutable
能让它的所有成员都变成mutable
类型:
int main() {
int i = 2;
auto ok = [i, x{22}]() mutable { i++; x++; };
}
不过需要注意的是,和被mutable
修饰的变量相比,mutable
的lambda表达式应该是一个不太常用的玩意儿。运用的时候需要特别小心,因为这种做法相对lambda表达式本身而言就是一种“code smell”,看看是不是你的代码设计需要重构了。
结语
mutable
一般都是const
的好朋友,虽然用它比用const
少得多,但是它绝对不是什么花里胡哨的技巧。它确实能够帮助我们优化代码,写出更安全,更健壮的const
正确的代码。