“可变”关键字除了允许 const 成员函数修改数据成员之外,还有其他用途吗?

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

提问人:Rob 提问时间:9/20/2008 最后编辑:Jan SchultkeRob 更新时间:6/14/2023 访问量:282096

问:

不久前,我遇到了一些使用关键字标记类的数据成员的代码。据我所知,它只是允许您在 -qualified 成员方法中修改成员:mutableconst

class Foo  
{  
private:  
    mutable bool done_;  
public:  
    void doSomething() const { ...; done_ = true; }  
};

这是这个关键词的唯一用途,还是它比眼前看到的要多?从那以后,我在一个类中使用了这种技术,将 标记为 ,允许函数出于线程安全原因锁定它,但老实说,这感觉有点像黑客。boost::mutexmutableconst

C++(英语:C++) 关键词 可变 DataMember

评论

3赞 Misgevolution 8/19/2016
但问题是,如果您不修改任何内容,为什么首先需要使用互斥锁?我只是想了解这一点。
0赞 iheanyi 5/23/2017
@Misgevolution您正在修改某些内容,您只需控制谁/如何通过 const 进行修改。一个非常幼稚的例子,想象一下,如果我只给朋友一个非常量句柄,敌人会得到一个常量句柄。朋友可以修改,敌人不能。
3赞 Gabriel Staples 8/6/2017
注意:这是使用关键字的一个很好的例子:stackoverflow.com/questions/15999123/...mutable
0赞 alfC 10/19/2019
我希望它可以用来覆盖(类型),这样我就不必这样做了:,如果我想要默认常量,即 (显式可变)和(隐式常量)。constclass A_mutable{}; using A = A_mutable const; mutable_t<A> a;mutable A a;A a;
5赞 Dexter 2/3/2022
@Misgevolution因为其他线程正在修改。

答:

81赞 Frank Szczerba #1

您与 boost::mutex 的使用正是此关键字的用途。另一个用途是内部结果缓存以加快访问速度。

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

在您问题的示例代码中,如果 done_ 的值影响外部状态,则 mutable 可能不合适,这取决于 ...;部分。

42赞 John Millikin 9/20/2008 #2

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

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

因此,如果上述疯狂不是什么 可变是干什么用的,它干什么用?这是 微妙的情况:可变的用于 对象在逻辑上是 常量,但在实践中需要 改变。这些案例很少,而且很遥远 之间,但它们存在。

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

评论

2赞 enthusiasticgeek 7/25/2012
我认为这个链接给出了可变是有帮助的场景的最佳示例。它们似乎专门用于调试。(根据正确用法)
0赞 Th. Thielemann 1/8/2019
的使用可以使代码更具可读性和更简洁。在以下示例中,可以按预期。'可变m_mutex;集装箱m_container;void add(Item item) { Lockguard lock(m_mutex); m_container.pushback(item); }item read() const { Lockguard lock(m_mutex); return m_container.first(); } 'mutablereadconst
1赞 Seva Alekseyev 5/5/2020
有一个非常流行的用例:ref counts。
1赞 Joe Schneider 9/20/2008 #3

在某些情况下(例如设计不佳的迭代器),类需要保留计数或其他一些附带值,这不会真正影响类的主要“状态”。这是我最常看到可变使用的地方。如果没有可变性,您将被迫牺牲设计的整体稳定性。

大多数时候对我来说,这感觉也像是一个黑客。在极少数情况下有用。

38赞 Adam Rosenfield 9/20/2008 #4

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

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 HashTablelookup()

评论

4赞 Adriel Jr 12/10/2021
这个例子很好,但这种做法隐藏着一个危险的后果。查看查找调用的人可能会认为它是线程安全的,因为它“不会”因为 const 限定符而更改对象的状态。后来,当事情不起作用时......为了找到比赛条件,数小时的工作被扔掉了。这是一种可怕的做法。
432赞 KeithB 9/20/2008 #5

It allows the differentiation of bitwise const and logical const. Logical const is when an object doesn't change in a way that is visible through the public interface, like your locking example. Another example would be a class that computes a value the first time it is requested, and caches the result.

Since c++11 can be used on a lambda to denote that things captured by value are modifiable (they aren't by default):mutable

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

评论

58赞 Richard Corden 9/22/2008
“可变”完全不影响按位/逻辑常量。C++ 只是按位常量,“可变”关键字可用于从此检查中排除成员。除了通过抽象之外,不可能在C++中实现“逻辑”常量(例如。SmartPtrs)。
141赞 Tony Delroy 7/29/2011
@Richard:你错过了重点。没有“逻辑常量”关键字,没错,它是一种概念上的区分,程序员根据对对象的逻辑可观察状态的理解,决定哪些成员应该通过可变来排除。
6赞 KeithB 3/6/2013
@ajay 是的,这就是将成员变量标记为可变变量的全部意义所在,以允许它在 const 对象中更改。
9赞 Giorgio 4/24/2013
为什么在 lambda 上需要可变?通过引用捕获变量还不够吗?
16赞 Sebastian Mach 6/5/2013
@Giorgio:区别在于 lambda 中的修改保留在 lambda 中,即 lambda 函数只能修改自己的副本。外面看不见变化,原来还是没有变化。考虑 lambda 是作为函子类实现的;捕获的变量对应于成员变量。xxx
7赞 Greg Rogers 9/20/2008 #6

可变主要用于类的实现细节。类的用户不需要知道它,因此他认为“应该”是常量的方法。你让互斥锁可变的例子就是一个很好的规范例子。

10赞 Shog9 9/20/2008 #7

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

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;
};

现在,您必须谨慎使用它 - 并发问题是一个大问题,因为调用者可能会认为如果仅使用方法,它们是线程安全的。当然,修改数据不应该以任何重要的方式改变对象的行为,例如,如果期望写入磁盘的更改对应用程序立即可见,那么我给出的示例可能会违反这一点。constmutable

5赞 JohnMcG 9/20/2008 #8

你对它的使用不是一个黑客,尽管就像C++中的许多东西一样,可变对于一个懒惰的程序员来说可能是黑客,他们不想一路回过头来,把不应该被const的东西标记为non-const。

12赞 Lloyd 9/20/2008 #9

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

其目的是,您可能有一个对对象的内部状态“不执行任何操作”的函数,因此您标记了该函数,但您可能确实需要以不影响其正确功能的方式修改某些对象状态。const

关键字可以作为编译器的提示 -- 理论编译器可以在内存中放置一个标记为只读的常量对象(例如全局对象)。存在不应这样做的提示。mutable

以下是声明和使用可变数据的一些正当理由:

  • 螺纹安全。声明 a 是完全合理的。mutable boost::mutex
  • 统计学。计算对函数的部分或全部参数的调用次数。
  • 记忆。计算一些昂贵的答案,然后将其存储以备将来参考,而不是再次重新计算。

评论

3赞 Richard Corden 9/22/2008
很好的答案,除了关于可变是“提示”的评论。这使得如果编译器将对象放入 ROM 中,则可变成员有时似乎是不可变的。可变的行为是明确定义的。
2赞 Hagen von Eitzen 8/19/2014
例如,除了将 const 对象放在只读内存中之外,编译器还可以决定优化循环之外的 const 功能调用。在其他常量函数中,可变统计计数器仍将允许此类优化(并且只计算一个调用),而不是仅仅为了计算更多调用而阻止优化。
1赞 BeeOnRope 6/19/2017
@HagenvonEitzen - 我很确定这是不正确的。编译器不能将函数从循环中提升出来,除非它能证明没有副作用。该证明通常涉及实际检查函数的实现(通常在函数内联之后)而不是依赖(并且无论 or 如何,这种检查都会成功或失败)。仅仅声明函数是不够的:函数可以自由地产生副作用,例如修改全局变量或传递给函数的内容,因此它不是该证明的有用保证。constconstmutableconstconst
1赞 BeeOnRope 6/19/2017
现在,一些编译器具有特殊的扩展,例如 gcc 的 _attribute__((const)) 和 __attribute__((pure)),它们_do具有这样的效果,但这只是与 C++ 中的关键字有切向关系。const
173赞 Dan L 3/5/2010 #10

关键字是一种刺穿您披在物体上的面纱的方法。如果具有指向某个对象的常量引用或指针,则无法以任何方式修改该对象,除非标记该对象的时间和方式。mutableconstmutable

使用您的引用或指针,您只能:const

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

例外情况使得您现在可以写入或设置标记为 的数据成员。这是唯一外部可见的差异。mutablemutable

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

在实践中,你可以在整个代码库中痴迷地使用(你本质上是想用“疾病”“感染”你的代码库)。在这个世界上,指针和引用只有极少数例外,产生的代码更容易推理和理解。对于一个有趣的题外话,请查找“参考透明度”。constconstconst

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

(注:我多次提到数据和方法可见性。我说的是标记为公共与私有或受保护的成员,这是此处讨论的完全不同类型的对象保护。

评论

20赞 Brian Bi 4/6/2015
此外,用于修改对象的一部分会产生未定义的行为。const_castconst
4赞 BeeOnRope 6/19/2017
我不同意,因为它迫使 API 客户端破坏对象的 const 保护。如果你用于在方法中实现成员变量的突变,你不会要求客户端进行强制转换 - 你会在方法中通过 ing 来完成。基本上,它允许您绕过特定呼叫站点上任意成员的常量,同时允许您删除所有呼叫站点上特定成员的常量。后者通常是您想要的典型用途(缓存、统计信息),但有时const_cast符合该模式。const_castconstconst_castthismutable
1赞 BeeOnRope 6/19/2017
在某些情况下,该模式确实更适合,例如,当您想要临时修改成员,然后恢复它时(非常像 )。该方法在逻辑上是常量的,因为最终状态与初始状态相同,但您希望进行瞬态更改。 在那里可能很有用,因为它可以让你在该方法中专门抛弃 const,如果你的突变将被撤消,但不太合适,因为它会从所有方法中删除 const 保护,这些方法不一定都遵循“做,撤消”模式。const_castboost::mutexconst_castmutable
3赞 BeeOnRope 6/19/2017
将常量定义的对象放置在只读存储器(更一般地说,标记为只读的存储器)中,以及允许这样做的相关标准语言,使得它成为可能的定时炸弹。 没有这样的问题,因为这些对象不能放在只读内存中。const_castmutable
2赞 Daniel Hershcovich 5/16/2011 #11

经典的例子(如其他答案中提到的)也是我迄今为止看到的关键字使用的唯一情况,是用于缓存复杂方法的结果,其中缓存被实现为类的数据成员,而不是方法中的静态变量(出于多个函数之间的共享或简单清洁的原因)。mutableGet

通常,使用关键字的替代方法通常是方法或技巧中的静态变量。mutableconst_cast

另一个详细的解释在这里

评论

1赞 Sebastian Mach 11/19/2013
我从未听说过使用静态成员作为可变成员的一般替代方案。并且仅适用于您知道(或已保证)某些内容不会更改(例如,当干扰 C 库时)或您知道它没有被声明为 const 时。即,修改 const-casted const 变量会导致未定义的行为。const_cast
1赞 Daniel Hershcovich 11/21/2013
@phresnel 我所说的“静态变量”是指方法中的静态自动变量(在调用之间保持)。并且可以用来修改方法中的类成员,这就是我提到的......const_castconst
1赞 Sebastian Mach 11/21/2013
我不清楚,因为你写的是“一般”:)关于修改 ,如前所述,只有在未声明对象时才允许这样做。例如,导致未定义的行为。const_castconstconst Frob f; f.something();void something() const { const_cast<int&>(m_foo) = 2;
10赞 mkschreder 6/28/2013 #12

当类中有一个变量时,使用 Mutable,该变量仅在该类中用于表示互斥锁或锁等内容。此变量不会改变类的行为,但对于实现类本身的线程安全是必需的。因此,如果没有“可变”,您将无法拥有“const”函数,因为该变量需要在外部世界可用的所有函数中更改。因此,引入了可变变量,以使成员变量即使由常量函数也可写。

指定的可变对象通知编译器和读取器,它 是安全的,并且预计成员变量可以在常量中修改 member 函数。

8赞 Zack Yezek 1/14/2014 #13

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

我最常使用的情况是无状态“纯旧数据”成员的延迟初始化。也就是说,在狭窄的情况下,当这些成员的构建(处理器)或随身携带(内存)都很昂贵,并且对象的许多用户永远不会要求它们时,它是理想的。在这种情况下,您希望在后端进行延迟构造以提高性能,因为 90% 构建的对象根本不需要构建它们,但您仍然需要提供正确的无状态 API 供公共使用。

2赞 Saurabh 2/1/2014 #14

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

-6赞 Rajdeep Rathore 2/22/2014 #15

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

//Prototype 
class tag_name{
                :
                :
                mutable var_name;
                :
                :
               };   
3赞 Martin G 5/27/2014 #16

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

5赞 Kevin Cox 7/25/2014 #17

Mutable 将类的含义从按位 const 更改为逻辑 const。const

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

此外,它还通过允许成员函数在不使用 .constconst_cast

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.
}

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

0赞 Venkatakrishna Kalepalli 1/26/2015 #18

我们使用 mutable 的最好例子之一是,在深拷贝中,在 copy 构造函数中,我们作为参数发送。因此,创建的新对象将具有常量类型。如果我们想更改(大多数情况下我们不会更改,在极少数情况下我们可能会更改)这个新创建的 const 对象中的成员,我们需要将其声明为 。const &objmutable

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

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

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

评论

1赞 underscore_d 7/2/2021
关于复制的段落毫无意义。复制构造函数和赋值运算符可以修改它们正在构造或分配的目标对象。他们应该没有理由修改源对象。源对象是 const& 与这些事实无关。