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正确的代码。