使用const_cast的未定义行为

Undefined behaviour with const_cast

本文关键字:未定义 cast const 使用      更新时间:2023-10-16

我希望有人能澄清c++中未定义行为的确切含义。给定以下类定义:

class Foo
{
public:
    explicit Foo(int Value): m_Int(Value) { }
    void SetValue(int Value) { m_Int = Value; }
private:
    Foo(const Foo& rhs);
    const Foo& operator=(const Foo& rhs);
private:
    int m_Int;
};

如果我理解正确的话,下面代码中对引用和指针的两个const_cast将删除Foo类型的原始对象的const-ness,但是任何通过指针或引用修改该对象的尝试都会导致未定义的行为。

int main()
{
    const Foo MyConstFoo(0);
    Foo& rFoo = const_cast<Foo&>(MyConstFoo);
    Foo* pFoo = const_cast<Foo*>(&MyConstFoo);
    //MyConstFoo.SetValue(1);   //Error as MyConstFoo is const
    rFoo.SetValue(2);           //Undefined behaviour
    pFoo->SetValue(3);          //Undefined behaviour
    return 0;
}

令我困惑的是,为什么这似乎可以工作,并且会修改原始的const对象,但甚至没有提示我警告,通知我这种行为是未定义的。我知道,一般来说,const_cast是不受欢迎的,但我可以想象,如果没有意识到c风格的强制转换,可能会导致const_cast在不被注意的情况下发生,例如:

Foo& rAnotherFoo = (Foo&)MyConstFoo;
Foo* pAnotherFoo = (Foo*)&MyConstFoo;
rAnotherFoo->SetValue(4);
pAnotherFoo->SetValue(5);

在什么情况下此行为可能导致致命的运行时错误?是否有一些编译器设置,我可以设置来警告我这种(潜在的)危险行为?

注:我用MSVC2008。

我希望有人能澄清c++中未定义行为的确切含义。

从技术上讲,"未定义行为"意味着语言没有定义做这种事情的语义。

在实践中,这通常意味着"不要这样做;当编译器执行优化或其他原因时,它可能会中断"。

令我困惑的是为什么这看起来是有效的,并且会修改原始的const对象,但甚至没有提示我警告,通知我这种行为是未定义的。

在这个特定的例子中,试图修改任何不可变对象可能"看起来有效",或者它可能覆盖不属于程序的内存或属于其他对象的[一部分],因为不可变对象可能在编译时被优化掉了,或者它可能存在于内存中的某些只读数据段中。

可能导致这些事情发生的因素太复杂了,无法一一列举。考虑解引用一个未初始化的指针(也UB)的情况:"对象"你然后工作将有一些任意的内存地址,这取决于任何值发生在内存中的指针的位置;这个"值"可能依赖于以前的程序调用、以前在同一个程序中的工作、用户提供的输入的存储等。试图合理化调用未定义行为的可能结果是不可行的,所以,我们通常不麻烦,而是说"不要这样做"。

更复杂的是,编译器不需要来诊断(发出警告/错误)未定义行为,因为调用未定义行为的代码与格式不良(即显式非法)的代码不同。在许多情况下,编译器甚至无法检测到UB,因此这是程序员负责正确编写代码的领域。

类型系统—包括const关键字&mdash的存在性和语义;提供基本的保护,防止编写可能会中断的代码;c++程序员应该始终意识到破坏这个系统—例如,通过破解const ness —你需要自己承担风险,这通常是一个坏主意。™

我可以想象这样一种情况:缺乏对c风格强制转换的认识,可能会导致在不被注意的情况下发生const_cast

绝对。如果将警告级别设置得足够高,一个正常的编译器可能会选择警告您,但它不必这样做,也可能不会这样做。一般来说,这就是为什么c风格的强制转换不受欢迎的一个很好的原因,但它们仍然支持c的向后兼容性。这只是其中一件不幸的事情。

未定义行为取决于对象诞生的方式,您可以在00:10:00左右看到Stephan解释它,但实际上,遵循以下代码:

void f(int const &arg)
{
    int &danger( const_cast<int&>(arg); 
    danger = 23; // When is this UB?
}

现在有两种情况调用f

int K(1);
f(k); // OK
const int AK(1); 
f(AK); // triggers undefined behaviour

综上所述,K出生时是非const,所以调用f时强制转换是可以的,而AK出生时是const,所以…

未定义的行为字面意思就是:没有被语言标准定义的行为。它通常发生在代码出错,但编译器无法检测到错误的情况下。捕获错误的唯一方法是引入运行时测试——这将损害性能。因此,语言规范告诉你不能做某些事情,如果你做了,那么任何事情都可能发生。

在写入常量对象的情况下,使用const_cast破坏编译时检查,有三种可能的情况:

  • 它被当作一个非常量对象,写入它会修改它;
  • 它被放置在写保护内存中,写入它会导致保护故障;
  • 它被(在优化期间)替换为嵌入在编译代码中的常量值,因此在写入它之后,它仍将具有其初始值。

在您的测试中,您以第一种场景结束—对象(几乎可以肯定)是在堆栈上创建的,而堆栈没有写保护。如果对象是静态的,您可能会发现您得到第二种情况,如果您启用了更多的优化,则会得到第三种情况。

一般来说,编译器不能诊断这个错误——没有办法判断(除了在像你这样非常简单的例子中)引用或指针的目标是否是常量。只有在可以保证安全的情况下才使用const_cast,这取决于您——对象不是常量,或者无论如何都不打算修改它。

令我困惑的是为什么这似乎有效

这就是未定义行为的含义。
它可以做任何事情,包括看起来工作。
如果你将优化级别提高到最大值,它可能会停止工作。

,但甚至没有提示一个警告,通知我这个行为是未定义的。

在进行修改时,对象是而不是 const。在一般情况下,它不能告诉对象最初是const,因此不可能警告你。即使是,每个语句都是单独求值的,而不参考其他语句(当查看这种警告生成时)。

其次,通过使用cast,你告诉编译器"我知道我在做什么,覆盖你所有的安全功能,就这样做"

例如,下面的工作很好:(或将看起来太(在鼻音类型的方式))

float aFloat;
int& anIntRef = (int&)aFloat;  // I know what I am doing ignore the fact that this is sensable
int* anIntPtr = (int*)&aFloat;
anIntRef  = 12;
*anIntPtr = 13;
我知道,一般来说,const_cast不适合

那是错误的看待他们的方式。它们是一种在代码中记录您正在做的需要由聪明人验证的奇怪事情的方法(因为编译器将毫无疑问地服从强制转换)。您需要一个聪明的人来验证的原因是它可能导致未定义的行为,但是您现在已经在代码中明确地记录了这一点(并且人们肯定会仔细查看您所做的工作)。

,但是我可以想象在没有意识到c风格强制转换的情况下,可能会导致const_cast在没有被注意到的情况下发生,例如:

在c++中不需要使用C风格强制转换。在最坏的情况下,c风格的强制转换可以用reinterpret_cast<>代替,但是在移植代码时,您需要查看是否可以使用static_cast<>。c++强制转换的意义在于使它们突出,以便您可以看到它们,并一眼看出危险强制转换与良性强制转换之间的区别。

一个经典的例子是试图修改const字符串字量,它可能存在于受保护的数据段中。

编译器可能出于优化原因将const数据放在内存的只读部分,并且试图修改这些数据将导致UB。

静态和常量数据通常存储在程序的其他部分,而不是局部变量。对于const变量,这些区域通常处于只读模式,以强制变量的const性。尝试在只读内存中写入会导致"未定义行为",因为反应取决于您的操作系统。"未定义的行为"意味着语言没有指定如何处理这种情况。

如果你想了解关于内存的更详细的解释,我建议你阅读这篇文章。这是一种基于UNIX的解释,但类似的机制在所有操作系统上都使用。