为什么在抛弃指向该对象的指针而不是UB后写入非const对象?

时间:2011-12-16 06:33:24

标签: c++ const undefined-behavior const-correctness const-cast

根据C ++标准,如果对象本身不是const,则可以从指针中抛弃const并写入对象。这就是:

 const Type* object = new Type();
 const_cast<Type*>( object )->Modify();

没关系,但是这个:

 const Type object;
 const_cast<Type*>( &object )->Modify();

是UB。

The reasoning is当对象本身为const时,允许编译器优化对它的访问,例如,不执行重复读取,因为重复读取对于不更改的对象没有意义

问题是编译器如何知道哪些对象实际上是const?例如,我有一个功能:

void function( const Type* object )
{
    const_cast<Type*>( object )->Modify();
}

并将其编译为静态库,编译器不知道它将被调用的对象。

现在调用代码可以执行此操作:

Type* object = new Type();
function( object );

它会很好,或者它可以做到这一点:

const Type object;
function( &object );

并且它将是未定义的行为。

编译器应该如何遵守这些要求?如果不使后者工作,它应该如何使前者工作?

5 个答案:

答案 0 :(得分:6)

当你说“在不使后者工作的情况下如何使前者工作?”一个实现只需要使前者工作,它不需要 - 除非它想帮助程序员 - 做出任何额外的努力,试图使后者不能以某种特定的方式工作。 未定义的行为为实现提供了自由,而不是义务。

举一个更具体的例子。在此示例中,在f()中,编译器可以在调用EvilMutate之前将返回值设置为10,因为cobj.member是const cobj的构造函数完成后可能随后不写入。即使只调用g()函数,它也无法在const中做出相同的假设。如果EvilMutatemember cobj中调用f()时发生const,则会发生未定义行为,并且实施不需要使任何后续操作具有任何特定内容影响。

编译器假定真正的struct Type { int member; void Mutate(); void EvilMutate() const; Type() : member(10) {} }; int f() { const Type cobj; cobj.EvilMutate(); return cobj.member; } int g() { Type obj; obj.EvilMutate(); return obj.member; } 对象不会改变的能力受到这样的事实的保护:这样做会导致未定义的行为;事实上,它并没有对编译器施加额外的要求,只对程序员提出了要求。

{{1}}

答案 1 :(得分:3)

编译器只能对const对象执行优化,而不能对const对象的引用/指针执行优化(请参阅this question)。在您的示例中,编译器无法优化function,但他可以使用const Type优化代码。由于编译器假定此对象是常量,因此修改它(通过调用function)可以执行任何操作,包括使程序崩溃(例如,如果对象存储在只读存储器中)或者像非-const版本(如果修改不干扰优化)

非const版本没有问题,并且是完美定义的,你只需要修改一个非const对象,这样一切都很好。

答案 2 :(得分:2)

如果对象被声明为const,则允许实现以这样的方式存储它,即尝试修改它可能导致硬件陷阱,而没有任何义务确保这些陷阱的任何特定行为。如果构造一个指向这样一个对象的const指针,那么通常不允许该指针的接收者写入它,因此没有触发那些硬件陷阱的危险。如果代码抛弃了const - 并且写入指针,编译器就没有义务保护程序员免受可能发生的任何硬件奇怪的影响。

此外,如果编译器可以告诉const对象总是包含特定的字节序列,它可以通知链接器,并允许链接器查看该序列是否字节出现在代码中的任何地方,如果是这样,将const对象的地址视为该字节序列的位置(遵守有关具有唯一地址的不同对象的各种限制可能有点棘手,但它是允许的)。如果编译器告诉链接器const char[4]总是应该包含恰好出现在某个函数的编译代码中的字节序列,则链接器可以将该字节序列中的地址分配给该变量出现。如果从未写过const,这样的行为会节省四个字节,但写入const会随意改变其他代码的含义。

如果在抛弃const之后写入对象总是UB,那么抛弃常量的能力将不会非常有用。实际上,这种能力通常在一段代码保留在指针上的情况下发挥作用 - 其中一些代码是const,其中一些代码需要写入 - 为了其他人的利益代码。如果丢弃const指向非const对象的指针的常量没有被定义的行为,那么持有指针的代码需要知道哪些指针是const哪些是需要写的。但是,因为允许const-casting,持有指针的代码将它们全部声明为const就足够了,并且对于知道指针标识非const对象并想要将其写入的代码来说,把它投射到非投射指针。

如果C ++具有const(和volatile)限定符的形式可能会有所帮助,这些限定符可用于指示编译器的指针(或者,在{{1}的情况下)即使编译器知道对象是,并且知道它不是{{1} }和/或未声明volatile 。前者允许编译器假设指针识别的对象在指针的生命周期内不会改变,并基于此缓存数据;后者允许在某些罕见的情况下(通常在程序启动时)变量可能需要支持const访问但在此之后编译器应该能够缓存其值的情况。但我知道没有提议添加这些功能。

答案 3 :(得分:1)

未定义的行为意味着未定义的行为。规范不保证会发生什么。

这并不意味着它不会做你打算的事情。只是你不在规范声明应该工作的行为边界之外。规范可以说明当你做某些事情会发生什么。在保护规范之外,所有赌注都已关闭。

但仅仅因为你不在地图的边缘并不意味着你会遇到一条龙。也许这将是一个蓬松的兔子。

这样想:

class BaseClass {};
class Derived : public BaseClass {};

BaseClass *pDerived = new Derived();
BaseClass *pBase = new Base();

Derived *pLegal = static_cast<Derived*>(pDerived);
Derived *pIllegal = static_cast<Derived*>(pBase);

C ++定义了其中一个强制转换。另一个产生未定义的行为。这是否意味着C ++编译器实际检查类型并翻转“未定义行为”开关?否。

这意味着C ++编译器很可能假设 pBase实际上是Derived,因此执行转换pBase所需的指针算法进入Derived*。如果 实际上不是Derived,则会得到未定义的结果。

指针算术实际上可能是无操作;它可能什么都不做。或者它实际上可能会做某事。没关系;您现在已超出规范定义的行为领域。如果指针算术是无操作,那么一切都可能看起来完美无缺。

并不是编译器“知道”在一个实例中它是未定义的而在另一个实例中它是已定义的。这是规范没有说会发生什么。它似乎工作。它可能不会。 工作的唯一时间是按照规范正确完成的时间。

同样适用于const演员阵容。如果const广告素材来自最初不是const的对象,那么规范会说它会起作用。如果不是,则规范说任何事情都可能发生。

答案 4 :(得分:0)

理论上,在某些情况下允许将const对象存储在只读内存中,如果您尝试修改对象,这会导致明显的问题,但更可能的情况是,如果在任何时候定义了对象是可见的,因此编译器实际上可以看到对象被定义为const,编译器可以基于该对象的成员不改变的假设进行优化。如果在const对象上调用非const函数来设置成员,然后读取该成员,则编译器可以绕过该成员的读取(如果它已经知道该值)。毕竟,您将对象定义为const:您承诺该值不会更改。

未定义的行为很棘手,因为它常常似乎按预期工作,直到您稍作修改。