繁体   English   中英

const_cast的未定义行为

[英]Undefined behaviour with 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。

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

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

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

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

在这个具体的例子中,尝试修改任何非可变对象可能“似乎工作”,或者它可能覆盖不属于该程序或属于某个其他对象的[部分]的内存,因为不可变对象可能在编译时已经过优化,或者可能存在于内存中的某些只读数据段中。

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

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

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

类型系统 - 包括const关键字的存在和语义 - 提供了基本保护,防止编写将被破坏的代码; 一个C ++程序员应该始终意识到破坏这个系统 - 例如通过破解const - 是由你自己承担风险,并且通常是一个坏主意。

我可以想象这样一种情况,即缺乏对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所以... UB就是这样。

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

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

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

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

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

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

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

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

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

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

例如,以下工作正常:(或似乎也是如此(在鼻腔deamon类型的方式))

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样式转换。
在最坏的情况下,C-Style强制转换可以被reinterpret_cast <>替换,但在移植代码时,您想要查看是否可以使用static_cast <>。 C ++演员阵容的目的是让他们脱颖而出,这样你就可以看到他们,并且一眼就看出了良性演员的危险演员之间的区别。

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

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

静态和常量数据通常存储在程序的另一部分而不是局部变量中。 对于const变量,这些区域通常处于只读模式以强制执行变量的常量。 尝试写入只读内存会导致“未定义的行为”,因为反应取决于您的操作系统。 “未定义的beheavior”表示该语言未指定如何处理此案例。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM