const_cast的未定义行为

时间:2011-09-08 14:21:07

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

我希望有人能够准确地澄清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_casts将删除Foo类型的原始对象的常量,但是通过指针或者指针修改此对象的任何尝试引用将导致未定义的行为。

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_casts在广义上说是不受欢迎的,但是我可以想象一种情况,即缺乏对C风格演员可能会导致const_cast的意识,而不会被注意到,例如:

Foo& rAnotherFoo = (Foo&)MyConstFoo;
Foo* pAnotherFoo = (Foo*)&MyConstFoo;

rAnotherFoo->SetValue(4);
pAnotherFoo->SetValue(5);

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

注意:我使用的是MSVC2008。

7 个答案:

答案 0 :(得分:11)

  

我希望有人能够准确地澄清C ++中未定义行为的含义。

从技术上讲,“未定义行为”意味着该语言没有定义用于执行此类操作的语义。

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

  

令我感到困惑的是为什么这似乎有效并将修改原始const对象但是甚至没有提示我发出警告通知我这种行为是未定义的。

在此特定示例中,尝试修改任何非可变对象可能“似乎工作”,或者它可能会覆盖不属于该程序或属于某个其他对象的[部分]的内存,因为非可变对象可能在编译时被优化掉了,或者它可能存在于内存中的某些只读数据段中。

可能导致这些事情发生的因素太复杂而无法列出。考虑解除引用未初始化指针(也是UB)的情况:您正在使用的“对象”将具有一些任意内存地址,该地址取决于在指针位置的内存中发生的任何值; “价值”可能依赖于之前的程序调用,以前在同一程序中的工作,存储用户提供的输入等。尝试合理化调用未定义行为的可能结果是不可行的,所以,我们通常不会请打扰,而只是说“不要这样做”。

  

令我感到困惑的是,为什么这似乎有用,并且会修改原始的const对象但是甚至没有提示我发出警告通知我这种行为是未定义的。

作为一个进一步的复杂性,编译器不需要 来诊断(发出警告/错误)未定义行为,因为调用未定义行为的代码与不正确的代码不同(即明确违法)。在许多情况下,编译器甚至无法检测UB,因此编程人员有责任正确编写代码。

类型系统 - 包括const关键字的存在和语义 - 提供基本保护,防止编写将破坏的代码;一个C ++程序员应该始终意识到颠覆这个系统 - 例如通过黑客攻击const来完成,风险自负,通常是一个坏主意。™

  

我可以想象这样一种情况,即C风格的演员表可能会导致const_cast无法被发现而不会被人注意。

绝对。在警告级别设置得足够高的情况下,理智的编译器可以选择警告您这一点,但它没有,也可能没有。一般来说,这是C风格演员阵容不赞成的一个很好的理由,但它们仍然支持与C的向后兼容性。这只是其中一个不幸的事情。

答案 1 :(得分:4)

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

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

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

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

通常,编译器无法诊断此错误 - 无法判断引用或指针的目标是否为常量(除非像您这样的非常简单的示例)。你可以确保只有在保证它是安全的时才使用const_cast - 无论是在物体不稳定的情况下,还是在你实际上都没有修改它的时候。

答案 2 :(得分:4)

  

让我感到困惑的是为什么这似乎有效

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

  

但是甚至没有提示我发出警告通知我这种行为是未定义的。

在修改时,对象是不是 const。在一般情况下,它无法判断该对象最初是一个const,因此无法警告您。即使是每个语句都是单独评估而不参考其他语句(在查看那种警告生成时)。

其次,通过使用演员,你告诉编译器“我知道我正在做什么覆盖你所有的安全功能,只是这样做”

例如,以下工作正常:(或者似乎也是如此(以鼻方式的方式))

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_casts在广义上说是不受欢迎的

这是看错它们的方法。它们是一种在代码中记录您正在做一些需要由聪明人验证的奇怪事物的方法(因为编译器会毫无疑问地遵守演员表)。你需要一个聪明的人来验证的原因是它可能会导致未定义的行为,但你现在已经在代码中明确记录了这一点(人们肯定会密切关注你所做的事情)。

  

但我可以想象一种情况,即缺乏对C风格演员可能会导致制作const_cast的认识可能会在没有被注意的情况下发生,例如:

在C ++中,不需要使用C样式转换 在最坏的情况下,可以用reinterpret_cast&lt;&gt;替换C样式演员表。但在移植代码时,您希望查看是否可以使用static_cast&lt;&gt;。 C ++演员的观点是让它们脱颖而出所以你可以看到它们,并且一眼就看出了良性演员的危险演员之间的区别。

答案 3 :(得分:4)

未定义的行为取决于对象的生成方式,您可以在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生成了一个非常数,所以在调用f时强制转换是正确的,而AK生成const所以...... UB就是。< / p>

答案 4 :(得分:2)

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

答案 5 :(得分:0)

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

答案 6 :(得分:0)

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

如果您想了解有关内存的更详细说明,建议您阅读this。这是基于UNIX的解释,但在所有操作系统上都使用了类似的机制。