10

C++ 标准说修改最初声明的对象const是未定义的行为。但是构造函数和析构函数是如何操作的呢?

class Class {
public:
    Class() { Change(); }
    ~Class() { Change(); }
    void Change() { data = 0; }
private:
    int data;
};

//later:
const Class object;
//object.Change(); - won't compile
const_cast<Class&>( object ).Change();// compiles, but it's undefined behavior

我的意思是这里的构造函数和析构函数与调用代码做的事情完全相同,但是它们被允许更改对象并且不允许调用者——他遇到了未定义的行为。

它应该如何在实现下并根据标准工作?

4

5 回答 5

16

该标准明确允许构造函数和析构函数处理const对象。从 12.1/4 “构造函数”开始:

可以为 或 对象调用const构造volatile函数const volatile。...constvolatile语义(7.1.5.1)不适用于正在构建的对象。此类语义仅在最派生对象 (1.8) 的构造函数结束后才生效。

和 12.4/2 “析构函数”:

可以为constvolatileconst volatile对象调用析构函数。... constvolatile语义(7.1.5.1)不适用于正在破坏的对象。一旦最派生对象(1.8)的析构函数开始,这种语义就停止生效。

作为背景,Stroustrup 在“C++ 的设计和演变”(13.3.2 定义的细化const)中说:

为了确保某些(但不是全部)const对象可以放入只读存储器 (ROM),我采用了以下规则:任何具有构造函数(即需要运行时初始化)的对象都不能放入 ROM,但其他const对象可以。

...

const从构造函数完成到析构函数开始,声明的对象被认为是不可变的。在这些点之间写入对象的结果被视为未定义。

最初设计const时,我记得我争论过理想const的对象是在构造函数运行之前是可写的,然后通过一些硬件魔法变为只读,最后在进入析构函数时再次变为可写。可以想象一种实际上以这种方式工作的标记架构。如果有人可以写入定义的对象,这样的实现将导致运行时错误const。另一方面,有人可以写入const作为const引用或指针传递的未定义对象。在这两种情况下,用户都必须先扔掉const。这种观点的含义是抛弃const最初定义的对象const然后写入它充其量是未定义的,而对最初未定义的对象执行相同操作const是合法且定义明确的。

请注意,通过对规则的这种细化, 的含义const不取决于类型是否具有构造函数。原则上,他们都这样做。现在声明的任何对象const都可以放在ROM中,放在代码段中,受到访问控制的保护等,以确保它在收到初始值后不会发生变异。然而,这种保护不是必需的,因为当前的系统通常不能保护每个const人免受各种形式的损坏。

于 2010-02-16T06:33:14.277 回答
2

详细说明 Jerry Coffin 所说的:该标准使访问 const 对象未定义,只有当该访问发生在对象的生命周期内时。

7.1.5.1/4:

除了可以修改任何声明为 mutable (7.1.1) 的类成员外,任何在 const 对象的生命周期 (3.8) 期间修改它的尝试都会导致未定义的行为。

对象的生命周期仅在构造函数完成后才开始。

3.8/1:

T 类型对象的生命周期开始于:

  • 获得具有适合类型 T 的对齐和大小的存储,并且
  • 如果 T 是具有非平凡构造函数(12.1)的类类型,则构造函数调用已完成。
于 2010-02-16T09:16:24.187 回答
1

该标准并没有真正说明实现如何使其工作,但基本思想非常简单:const适用于object,而不是(必然)适用于存储 object 的内存。由于 ctor 是创建对象的一部分,因此直到(不久之后)ctor 返回之前,它并不是真正的对象。同样,由于 dtor 参与销毁对象,它也不再真正对完整对象进行操作。

于 2010-02-16T06:35:54.743 回答
1

这是一种忽略标准可能导致错误行为的方法。考虑这样的情况:

class Value
{
    int value;

public: 
    value(int initial_value = 0)
        : value(initial_value)
    {
    }

    void set(int new_value)
    {
        value = new_value;
    }

    int get() const
    {
        return value;
    }
}

void cheat(const Value &v);

int doit()
{
    const Value v(5);

    cheat(v);

    return v.get();
}

如果优化,编译器知道 v 是 const 所以可以用 替换v.get()调用5

但是,假设在不同的翻译单元中,您的定义cheat()如下:

void cheat(const Value &cv)
{
     Value &v = const_cast<Value &>(cv);
     v.set(v.get() + 2);
}

因此,虽然在大多数平台上都会运行,但行为可能会根据优化器的作用而改变。

于 2010-02-16T08:10:47.390 回答
0

用户定义类型的常量与内置类型的常量不同。当与用户定义的类型一起使用时,常量被称为“逻辑常量”。编译器强制只能在 const 对象(或指针或引用)上调用声明为“const”的成员函数。编译器不能在某些只读内存区域分配对象,因为非const成员函数必须能够修改对象的状态(甚至const成员函数必须能够在声明成员变量时mutable)。

对于内置类型,我相信允许编译器在只读内存中分配对象(如果平台支持这样的事情)。因此抛弃 const 并修改变量可能会导致运行时内存保护错误。

于 2010-02-16T06:34:52.960 回答