10

我希望有人能准确地阐明 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 类型的原始对象的 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_casts 从广义上讲是不受欢迎的,但我可以想象这样一种情况,即缺乏对 C 风格转换可能导致 const_cast 的认识的情况可能会在不被注意的情况下发生,例如:

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

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

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

注意:我使用 MSVC2008。

4

7 回答 7

11

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

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

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

令我困惑的是为什么这似乎有效并且会修改原始的 const 对象,但甚至不会提示我警告以通知我此行为未定义。

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

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

令我困惑的是为什么这似乎有效并且会修改原始的 const 对象,但甚至不会提示我警告以通知我此行为未定义。

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

类型系统——包括const关键字的存在和语义——提供了防止编写会破坏的代码的基本保护;C++ 程序员应该始终意识到,颠覆这个系统(例如通过破解constness)需要您自担风险,而且通常是个坏主意。™</p>

我可以想象这样一种情况,即缺乏对 C 风格演员可能会导致const_cast制作的意识可能会在不被注意的情况下发生。

绝对地。警告级别设置得足够高,一个理智的编译器可能会选择警告你,但它不是必须的,也可能不是。一般来说,这是不赞成 C 风格转换的一个很好的理由,但它们仍然支持与 C 的向后兼容性。这只是那些不幸的事情之一。

于 2011-09-08T14:32:29.237 回答
4

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

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

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

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

通常,编译器无法诊断此错误 - 无法判断(除了在像您这样的非常简单的示例中)引用或指针的目标是否为常量。由您来确保仅const_cast在可以保证它是安全的情况下使用 - 无论是当对象不是常量时,还是当您实际上不打算修改它时。

于 2011-09-08T14:39:06.727 回答
4

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

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

但甚至不会提示我警告以通知我此行为未定义。

在这一点上,它做了修改,对象不是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_casts 从广义上讲是不受欢迎的

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

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

在 C++ 中,不需要使用 C 样式转换。
在最坏的情况下,C-Style cast 可以被 reinterpret_cast<> 替换,但是在移植代码时你想看看你是否可以使用 static_cast<>。C++ 强制转换的重点是让它们脱颖而出,以便您可以看到它们,并且一眼就能看出危险强制转换和良性强制转换之间的区别。

于 2011-09-08T14:59:02.753 回答
4

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

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 它是。

于 2014-03-08T16:55:05.577 回答
2

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

于 2011-09-08T14:24:48.590 回答
0

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

于 2011-09-08T14:28:05.487 回答
0

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

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

于 2011-09-08T14:35:08.523 回答