漫谈C++——const真理大讨论之 mutable

C++学习笔记

Posted by Felix Zhang on November 2, 2020

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