逻辑和物理常量性之间的区别

34

这两个术语有什么区别,我为什么需要使用mutable

5个回答

49

"物理"常量性是由声明一个const对象来实现的,原则上可以通过将对象放置在只读内存中来强制执行它从而防止其更改。试图更改它将导致未定义的行为;它可能会更改、也可能不会、也可能触发保护故障、或者可能熔化内存芯片。

“逻辑”常量性来自于声明一个引用或指针const,并由编译器强制执行。对象本身可能是“物理”常量,也可能不是,但是引用不可以被使用来修改它,除非进行转换。如果对象不是“物理”常量,那么C++允许你修改它,使用const_cast绕过保护。

mutable类成员即使类对象本身(或用于访问它的引用或指针)是const,也可以被修改。好的使用示例包括必须在读操作期间锁定的互斥量和缓存以存储昂贵读操作的结果。在这两种情况下,操作本身应该是一个const函数(因为它不影响对象的可见状态),但它需要修改互斥量或缓存,因此它们需要是mutable。它也可能被滥用,使对象在逻辑上不应该改变时可见地发生变化,因此要谨慎使用;只有声明的成员不属于外部可见状态,则将其声明为mutable


2
那读起来很好,但实际上是非常错误的。 "物理"常量性源于声明对象为const—— const不能保证您的"物理常量性":成员仍可能是mutable并在运行时更改。因为这是允许的,所以编译器/链接器等默认情况下不会将此类对象放置在只读内存中,即使它们是静态的const。如果通过某些非标准编译器/链接器/加载器标志强制将它们放置在那里,则程序员必须手动避免使用mutable成员。您所绘制的对象和指针/引用之间的区别实际上是无关紧要的。 - Tony Delroy
2
@TonyDelroy - 我认为Mike的回答涉及“将对象放置在只读内存中”的说法是一种修辞(并且在修辞上很有效),旨在阐明物理和逻辑const之间的差异,而不是一个关于编译器实际执行此操作的声明;他也没有说过编译器这样做。此外,我不同意他对指针和引用之间区别的看法是无关紧要的。这个答案在概念上是坚实而正确的。在我看来,TonyDelroy在声称这个答案“非常错误”时是不正确的。 - Dan Nissenbaum
1
一个可变的类成员可以被修改,即使类本身(或用于访问它的引用或指针)是const。类不是常量。只有一个实例可以是const。符号的类型描述了从该符号名称看到的实例的常量性。否则是一个很好的解释。 - Adrian
@Adrian:是的,那就是我想表达的意思。抱歉措辞有点含糊。 - Mike Seymour
你非常清楚地阐述了这个概念,为此点赞。 - HDJEMAI

19

当一个对象中存在可以被认为是“内部”的字段时,需要使用mutable。也就是说,类的任何外部用户都不能确定这些字段的值。虽然实例被视为常量,即不变化,但类可能需要对这些字段进行写操作。

考虑硬盘;它的缓存就是这种状态的一个例子。从实际磁盘读取数据时,会向缓存中写入数据。

在C++中,如果不将相应成员标记为mutable,则无法清晰地表达这一点,以允许即使在标记了const的方法中也能够更改它们的值。正如评论中指出的那样,您可以总是拿起锤子并使用const_cast<> 来删除const属性,但这当然是作弊。 :)


5
另一个类似的例子是在被不同线程共享的对象中使用互斥量。读取操作不会修改对象,但在过程中必须获取和释放互斥量。即使操作结果不会实质性地修改对象,互斥量成员对象在操作过程中也明显发生了变化。如果感知状态保持不变,即使内部元素发生改变,操作在对象上也是const的。 - David Rodríguez - dribeas
能否给我一个小的例子(代码),展示一下使用mutable可以做什么,而用const则不能做什么,并且同时说明逻辑常量和物理常量之间的区别?我在谷歌上找到了一些解释,但是没有找到好的例子。由于缓存的作用,有一些难以理解。 - rookie
这在C++中是不可能表达的,除非将相应的成员设置为mutable。那个说法是错误的,因为可以通过强制转换对象或成员来实现(过去已经实现过)。关键字mutable是在之后引入的,当程序员手动移除对象的constness以获得此效果时,逻辑上的constness的概念才被意识到,使其更易于维护和执行。请参见这里 - Adrian
@Adrian 谢谢,我添加了一些关于 const_cast<> 的内容。 - unwind

6

Scott Meyers在他的书《Effective C++》中提到:

尽可能使用const

这个话题,他有很好的讨论和示例。很难比Scott更好地写出来!

还要注意,物理常量也被称为位常量。


1

以下是一些代码,供想要尝试物理常量效果的人使用:

#include <type_traits>

using namespace std;

using type = int const;
type &f();

int main()
{
    const_cast<remove_const_t<type> &>(f()) = 123;
}

#if defined(__GNUC__) || defined(__llvm__)
__attribute__((noinline))
#elif defined(_MSC_VER)
__declspec(noinline)
#endif
type &f()
{
    static type i = 0;
    return i;
}

在Linux和Windows下,这个程序会崩溃,直到你去掉类型的const限定符。

1

我对Mike Seymour的回答持有不同意见:

一个类的成员函数可以被定义为const函数,这意味着它不会修改对象的任何内部状态(除了那些标记为mutable的成员,在我们后面谈到“逻辑”const-ness时变得很重要)。就是“物理”const-ness的实际定义。但是即使一个函数被标记为const,它可能在逻辑上修改对象的状态。考虑以下示例:

#include <iostream>
using namespace std;

class Foo
{
private:
    int *ip;

public:

    Foo(int i) {
        ip = new int(i);
    }

    virtual ~Foo() {
        delete ip;
    }

    void modifyState(int i) const {
        *ip = i;
    }

    int getI(void) const {
        return *ip;
    }
};

int main(int argc, char *argv[])
{
    Foo foo(7);
    cout << foo.getI() << endl;
    foo.modifyState(9);
    cout << foo.getI() << endl;
    return 0;
}

输出:

7
9

成员函数modifyState不会修改成员变量ip,因此它满足物理const性质的要求。但谁能否认,由于它正在修改ip指向的内容,逻辑状态并没有发生改变呢?因此,尽管该函数满足了物理const性质的要求,但它不满足逻辑const性质的要求,因为调用modifyStategetI函数返回的内容已经被更改。

相反,我们可以在物理上修改一个对象的成员,但该对象在其接口中向外部世界展现的状态并没有改变。考虑以下示例,其中const函数getCelsiusTemperaturegetFahrenheitTemperature修改了mutable状态,但并未修改对象的逻辑状态:

#include <iostream>
using namespace std;

class Temperature
{
private:
    mutable float temperature;
    mutable bool is_celsius;

public:

    Temperature(double temperature, bool is_celsius=true) {
        this->temperature = temperature;
        this->is_celsius = is_celsius;
    }

    double getCelsiusTemperature(void) const {
        if (!this->is_celsius) {
            // convert to Celsius:
            this->temperature = (this->temperature - 32.0) * 5.0 / 9.0;
            this->is_celsius = true;
        }
        return this->temperature;
    }

    const double getFahrenheitTemperature(void) const {
        if (this->is_celsius) {
            // convert to Fahrenheit:
            this->temperature = this->temperature * 9.0 / 5.0 + 32.0;
            this->is_celsius = false;
        }
        return this->temperature;
    }

};

int main(int argc, char *argv[])
{
    const Temperature t(100, true);
    cout << t.getCelsiusTemperature() << endl;
    cout << t.getFahrenheitTemperature() << endl;
    cout << t.getCelsiusTemperature() << endl;
    return 0;
}

输出:

100
212
100

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接