6

根据 C++ 标准,如果对象最初不是本身,则可以放弃const指针并写入对象。const这样:

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

没关系,但是这个:

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

是UB。

原因是当对象本身是const允许编译器优化对它的访问时,例如,不执行重复读取,因为重复读取对不变的对象没有意义。

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

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

它被编译成一个静态库,编译器不知道它将被调用哪些对象。

现在调用代码可以这样做:

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

它会很好,或者它可以这样做:

const Type object;
function( &object );

这将是未定义的行为。

编译器应该如何遵守这些要求?如何在不让后者工作的情况下让前者工作?

4

5 回答 5

6

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

举一个更具体的例子。在这个例子中f(),编译器可能在调用之前将返回值设置为 10,EvilMutate因为cobj.memberis const oncecobj的构造函数已经完成,随后可能不会被写入。g()即使只const调用一个函数,它也不能做出相同的假设。如果在未定义的行为中调用时发生EvilMutate变异尝试,并且实现不需要使任何后续操作具有任何特定效果。membercobjf()

编译器假设一个真正的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; 
}
于 2011-12-16T10:40:48.217 回答
3

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

非常量版本没有问题并且被完美定义,您只需修改一个非常量对象,一切都很好。

于 2011-12-16T09:14:40.117 回答
2

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

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

如果在抛弃后写入对象const始终是 UB,那么抛弃 const-ness 的能力不会很有用。事实上,这种能力通常在一段代码持有指针的情况下发挥作用——其中一些是指针,其中const一些需要编写——以使其他代码受益。如果抛弃const指向非const对象的指针的常量性不是定义的行为,那么持有指针的代码将需要知道哪些指针是指针,哪些指针const需要编写。但是,因为允许进行 const 转换,所以保存指针的代码将它们全部声明为const,对于知道指针标识非常量对象并想要编写它的代码,将其强制转换为非强制转换指针。

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

于 2015-07-23T16:13:50.910 回答
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实际上是 a ,因此执行将 a 转换为 aDerived所需的指针算术。如果它实际上不是a ,那么您会得到未定义的结果。pBaseDerived*Derived

该指针算术实际上可能是空操作;它可能什么都不做。或者它实际上可能会做一些事情。没关系; 您现在超出了规范定义的行为范围。如果指针算术是空操作,那么一切似乎都可以正常工作。

并不是编译器“知道”在一种情况下它是未定义的,而在另一种情况下它是已定义的。规范没有说明会发生什么。它可能看起来有效。它可能不会。只有当它按照规范正确完成时才会起作用。

const演员表也是如此。如果const演员表不是来自最初的对象const,那么规范说它会起作用。如果不是,那么规范说任何事情都可能发生。

于 2011-12-16T06:37:18.313 回答
0

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

未定义的行为很棘手,因为它通常看起来像您期望的那样工作,直到您进行一点修改。

于 2011-12-16T06:43:51.403 回答