关于C++:”可变”关键字除了允许变量被const函数修改之外,还有什么用途吗?

Does the 'mutable' keyword have any purpose other than allowing the variable to be modified by a const function?

不久前,我遇到了一些用mutable关键字标记类的成员变量的代码。据我所知,它只允许您修改const方法中的变量:

1
2
3
4
5
6
7
class Foo  
{  
private:  
    mutable bool done_;  
public:  
    void doSomething() const { ...; done_ = true; }  
};

这是这个关键字的唯一用法,还是它有更多的东西,而不是符合眼睛?后来我在一个类中使用了这种技术,将boost::mutex标记为可变的,允许const函数出于线程安全的原因将其锁定,但老实说,这有点像黑客。


它允许区分位常量和逻辑常量。逻辑常量是当一个对象没有以一种通过公共接口可见的方式改变时,就像锁示例一样。另一个例子是一个类,它在第一次被请求时计算一个值,并缓存结果。

因为C++ 11 EDCOX1×1可用于lambda来表示由值捕获的事物是可修改的(它们不是默认的):

1
2
3
int x = 0;
auto f1 = [=]() mutable {x = 42;};  // OK
auto f2 = [=]()         {x = 42;};  // Error: a by-value capture cannot be modified in a non-mutable lambda


mutable关键字是一种穿透你覆盖在物体上的const面纱的方法。如果您有一个常量引用或指向某个对象的指针,则不能以任何方式修改该对象,除非该对象何时以及如何标记为mutable

使用您的const引用或指针,您将被限制为:

  • 仅对任何可见数据成员进行读取访问
  • 只调用标记为const的方法的权限。

mutable异常使得您现在可以编写或设置标记为mutable的数据成员。这是唯一的外部可见的区别。

在内部,那些对您可见的const方法也可以写入标记为mutable的数据成员。基本上,警察的面纱被全面刺穿。完全取决于API设计师,以确保mutable不会破坏const概念,并且仅在有用的特殊情况下使用。mutable关键字有帮助,因为它清楚地标记了受这些特殊情况影响的数据成员。

实际上,您可以在整个代码库中强制使用const(您基本上希望用const疾病"感染"您的代码库)。在这个世界上,指针和引用都是const,只有很少的例外,生成的代码更容易理解和解释。要了解有趣的题外话,请查阅"参考透明度"。

如果没有mutable关键字,最终将强制使用const_cast来处理它允许的各种有用的特殊情况(缓存、引用计数、调试数据等)。不幸的是,const_castmutable具有更大的破坏性,因为它迫使API客户端破坏const对其使用的对象的保护。此外,它还会导致广泛的const破坏:const_cast使用常量指针或引用允许对可见成员进行无限制的写入和方法调用访问。相反,mutable要求API设计器对const异常进行细粒度控制,通常这些异常隐藏在对私有数据进行操作的const方法中。

(注意,我指的是数据和方法可见性几次。我说的是标记为public与private或protected的成员,这是这里讨论的完全不同的对象保护类型。)


使用boost::mutex正是这个关键字的目的。另一个用途是内部结果缓存以加速访问。

基本上,"mutable"适用于任何不影响对象外部可见状态的类属性。

在您所讨论的示例代码中,如果done的值影响外部状态,则mutable可能不合适,这取决于…;部分中的内容。


mutable用于从const方法中将特定属性标记为可修改。这是它唯一的目的。在使用它之前要仔细考虑,因为如果您更改设计而不是使用mutable,您的代码可能会更干净、更可读。

网址:http://www.highprogrammer.com/alan/rants/mutable.html

So if the above madness isn't what
mutable is for, what is it for? Here's
the subtle case: mutable is for the
case where an object is logically
constant, but in practice needs to
change. These cases are few and far
between, but they exist.

作者给出的例子包括缓存和临时调试变量。


它在隐藏内部状态(如缓存)的情况下很有用。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class HashTable
{
...
public:
    string lookup(string key) const
    {
        if(key == lastKey)
            return lastValue;

        string value = lookupInternal(key);

        lastKey = key;
        lastValue = value;

        return value;
    }

private:
    mutable string lastKey, lastValue;
};

然后你可以让一个const HashTable对象仍然使用它的lookup()方法来修改内部缓存。


是的,就是这样。我将它用于由不在逻辑上更改类状态的方法修改的成员,例如,通过实现缓存来加快查找速度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CIniWrapper
{
public:
   CIniWrapper(LPCTSTR szIniFile);

   // non-const: logically modifies the state of the object
   void SetValue(LPCTSTR szName, LPCTSTR szValue);

   // const: does not logically change the object
   LPCTSTR GetValue(LPCTSTR szName, LPCTSTR szDefaultValue) const;

   // ...

private:
   // cache, avoids going to disk when a named value is retrieved multiple times
   // does not logically change the public interface, so declared mutable
   // so that it can be used by the const GetValue() method
   mutable std::map<string, string> m_mapNameToValue;
};

现在,您必须谨慎地使用它——并发性问题是一个很大的问题,因为调用方可能会假设它们是线程安全的,如果只使用const方法的话。当然,修改mutable数据不应该以任何显著的方式改变对象的行为,例如,如果预期写入磁盘的更改会立即对应用程序可见,我给出的示例可能会违反这一点。


mutable确实存在,因为您可以推断它允许修改其他常量函数中的数据。

其目的是您可能拥有一个对对象的内部状态"不做任何事情"的函数,因此您标记了函数const,但您可能确实需要以不影响其正确功能的方式修改某些对象状态。

关键字可以作为编译器的提示——理论上的编译器可以将一个常量对象(如全局对象)放在标记为只读的内存中。mutable的出现暗示了不应该这样做。

以下是声明和使用可变数据的一些有效原因:

  • 线程安全性。宣布一个mutable boost::mutex是完全合理的。
  • 统计学。对函数的调用数进行计数,给出函数的部分或全部参数。
  • 记忆化。计算一些昂贵的答案,然后将其存储以供将来参考,而不是重新计算。


当类中有一个变量,该变量只在该类中用于表示诸如互斥或锁之类的东西时,就使用mutable。这个变量不会改变类的行为,但对于实现类本身的线程安全是必需的。因此,如果没有"mutable",您将无法拥有"const"函数,因为需要在外部世界可用的所有函数中更改此变量。因此,为了使成员变量可写(即使是通过const函数),引入了可变变量。

The mutable specified informs both the compiler and the reader that it
is safe and expected that a member variable may be modified within a const
member function.


mutable主要用于类的实现细节。类的用户不需要知道它,因此方法的用户认为"应该"是常量。您的mutex是可变的示例是一个很好的规范示例。


你使用它不是黑客,尽管像C++中的很多东西一样,对于懒惰的程序员来说,可变的是可以破解的,他们不想一路回过头来标记不应该成为常量的东西。


对于逻辑上对用户无状态(因此在公共类的API中应该有"const"getter)但在底层实现(在.cpp中的代码)中不是无状态的东西,请使用"mutable"。

我最经常使用它的情况是懒惰地初始化没有状态的"普通老数据"成员。也就是说,在狭隘的情况下,当这些成员构建(处理器)或携带(内存)的成本很高,并且对象的许多用户永远不会要求它们时,这是理想的。在这种情况下,为了提高性能,您需要在后端进行懒惰的构造,因为90%的已构建对象根本不需要构建它们,但是您仍然需要为公共消费提供正确的无状态API。


经典的例子(如其他答案中提到的)和我迄今为止所看到的唯一的情况是缓存一个复杂的Get方法的结果,其中缓存是作为类的数据成员实现的,而不是作为方法中的静态变量实现的(因为在多个函数之间共享)或者简单的清洁)。

一般来说,使用mutable关键字的替代方法通常是方法或const_cast技巧中的静态变量。

这里还有一个详细的解释。


我们使用可变的一个最好的例子是,在深度复制中。在复制构造函数中,我们发送const &obj作为参数。因此创建的新对象将是常量类型。如果我们想改变(大多数情况下我们不会改变,很少情况下我们可能会改变)这个新创建的const对象中的成员,我们需要声明它为mutable

mutable存储类只能用于类的非静态非常量数据成员。类的可变数据成员可以修改,即使它是声明为const的对象的一部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Test
{
public:
    Test(): x(1), y(1) {};
    mutable int x;
    int y;
};

int main()
{
    const Test object;
    object.x = 123;
    //object.y = 123;
    /*
    * The above line if uncommented, will create compilation error.
    */
 

    cout<<"X:"<< object.x <<", Y:" << object.y;
    return 0;
}

Output:-
X:123, Y:1

在上面的示例中,我们可以更改成员变量x的值,尽管它是声明为const的对象的一部分。这是因为变量x被声明为可变的。但是如果您试图修改成员变量y的值,编译器将抛出一个错误。


当您重写一个常量虚函数并希望修改该函数中的子类成员变量时,可变变量非常方便。在大多数情况下,您不希望更改基类的接口,因此必须使用自己的可变成员变量。


可变关键字在为类测试目的创建存根时非常有用。您可以截取一个const函数,并且仍然能够增加(可变)计数器或您添加到存根中的任何测试功能。这样可以保持存根类的接口完好无损。


在某些情况下(比如设计糟糕的迭代器),类需要保留一个计数或其他一些偶然值,这不会真正影响类的主要"状态"。这是我最常看到的可变使用。如果没有可变的,你就不得不牺牲你设计的整个常量。

对我来说,大部分时间都是一次黑客行为。在极少数情况下有用。


mutable将类的const的含义从按位常量更改为逻辑常量。

这意味着具有可变成员的类将不再是位常量,并且不再出现在可执行文件的只读部分中。

此外,它还通过允许const成员函数在不使用const_cast的情况下更改可变成员来修改类型检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Logical {
    mutable int var;

public:
    Logical(): var(0) {}
    void set(int x) const { var = x; }
};

class Bitwise {
    int var;

public:
    Bitwise(): var(0) {}
    void set(int x) const {
        const_cast<Bitwise*>(this)->var = x;
    }
};

const Logical logical; // Not put in read-only.
const Bitwise bitwise; // Likely put in read-only.

int main(void)
{
    logical.set(5); // Well defined.
    bitwise.set(5); // Undefined.
}

有关更多详细信息,请参阅其他答案,但我想强调的是,它不仅适用于类型安全,而且会影响编译结果。


关键字"mutable"实际上是一个保留关键字。它通常用于更改常量变量的值。如果要有一个constsnt的多个值,请使用关键字mutable。

1
2
3
4
5
6
7
8
//Prototype
class tag_name{
                :
                :
                mutable var_name;
                :
                :
               };