const_cast的未定义行为

11

我希望有人能够澄清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属性,但是任何尝试通过指针或引用修改此对象的操作都将导致未定义的行为。

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。

1
可能是这个const_cast的行为未定义吗?的重复问题。 - spraff
1
如果你正在使用C风格的转换,任何一个好的编译器都应该警告你“丢弃了限定符”。请确保始终启用所有警告。 - Kerrek SB
5
“doesn't even prompt me with a warning to notify me that this behaviour is undefined” 可以翻译为“甚至不提供警告提示来告知我这种行为是未定义的”,降低你的期望值。C和C++编译器只能注意到极少数导致未定义行为的事情。使用const_cast或C风格转换意味着,“我知道我正在做危险的事情,不要阻止我”。C++编译器和程序员的一般哲学是,在这里发出警告并没有用处,因为代码中存在const_cast已经足够警告了。 - Steve Jessop
顺便说一下,有一种有效的(虽然很烦人且罕见)使用方法,那就是如果您将对MyConstFoo的引用传递到某些代码中,实际上不会修改它(并且已记录为不这样做等),但该代码并未考虑const安全性,因此需要使用Foo&而不是const Foo&。指针/引用的强制转换是危险的,并且用于危险的事情,因此需要额外的审查,这是避免使用它们的原因之一。尽管如此,有时候一些第三方库,特别是接受非const指针的旧C接口,会让您感到沮丧。 - Steve Jessop
参见:c++ - Is const_cast safe? - Stack Overflow 以及评论中链接的问题。 - user202729
显示剩余2条评论
7个回答

11
我希望有人能澄清C++中未定义行为的确切含义。
从技术上讲,“未定义行为”意味着语言没有定义执行此操作的语义。
实际上,这通常意味着“不要这样做”,因为它可能会在编译器执行优化或其他原因时出错。
令我困惑的是,为什么这似乎可以工作并修改原始const对象,但甚至没有提示我该行为是未定义的警告。
在这个特定的例子中,尝试修改任何非可变对象可能“似乎有效”,或者可能覆盖不属于程序的内存或属于某个其他对象的[部分]内存,因为非可变对象可能已经在编译时被优化掉了,或者它可能存在于一些只读数据段中的内存中。
导致这些事情发生的因素太复杂而无法列举。考虑解引用未初始化指针的情况(也是UB):然后您正在使用的“对象”将具有取决于指针位置处存储的任何值的任意内存地址;该“值”可能取决于先前的程序调用、同一程序中的先前工作、用户提供输入的存储等。因此,尝试理性地推断调用Undefined Behaviour的可能结果是不可行的,因此我们通常不费心思而只是说“不要这样做”。
令我困惑的是,为什么这似乎可以工作并修改原始const对象,但甚至没有提示我该行为是未定义的警告。
进一步复杂化的是,编译器不需要为Undefined Behaviour进行诊断(发出警告/错误),因为调用Undefined Behaviour的代码与非法形式的代码(即明确非法的代码)不同。在许多情况下,编译器甚至无法检测到UB,因此写好代码是程序员的责任。
类型系统 - 包括const关键字的存在和语义 - 提供了防止编写会导致错误的代码的基本保护;C++程序员应始终注意,破坏此系统 - 例如通过去掉constness - 是自担风险的,并且通常是一个坏主意。
我可以想象一种情况,在这种情况下,可能会在不被注意的情况下发生C样式转换可能导致执行const_cast。
绝对可以。如果警告级别足够高,则理智的编译器可能会选择警告您,但它不必这样做,也可能不会这样做。总的来说,这是C样式转换被不赞成的一个很好的原因,但它们仍然得到支持,以与C向后兼容。这只是其中的一些不幸的事情。

5

未定义行为取决于对象的生成方式,可以在大约00:10:00处看到Stephen解释,但基本上,请遵循以下代码:

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,所以会导致未定义行为。

这似乎遵循了在Is it Undefined Behaviour to cast away the constness of a function parameter?中详细描述的C语义。C++也是如此吗?引用C++标准会很好。 - jww

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_cast被普遍认为是不好的做法,但这种看法是错误的。它们是一种在代码中记录你正在执行某些需要由聪明人验证的奇怪操作的方式(因为编译器将不加质疑地遵循强制转换)。你需要一个聪明的人来验证的原因是它可能导致未定义的行为,但好的事情是现在你已经在代码中明确地记录了这一点(而且人们一定会仔细看你所做的事情)。
我可以想象一种情况,在这种情况下,使用C风格的转换可能会导致const_cast而不被注意到,例如:
在C++中,没有必要使用C样式的转换。在最坏的情况下,C风格的转换可以被reinterpret_cast代替,但在移植代码时,您需要查看是否可以使用static_cast。C++转换的重点是使它们醒目,以便您可以看到它们,并快速区分危险的转换和良性的转换。

4
未定义行为字面意思就是:语言标准未定义的行为。通常发生在代码出现问题但编译器无法检测到错误的情况下。唯一能捕捉到错误的方法是引入运行时测试,但这会降低性能。因此,语言规范告诉你不能做某些事情,如果你做了,那么任何事情都可能发生。
在写入常量对象的情况下,使用const_cast来规避编译时检查,有三种可能的情况:
1.它被视为非常量对象,并且对其进行写入将修改它; 2.它被放置在写保护内存中,对其进行写入会导致保护错误; 3.它被替换(在优化期间)为嵌入到编译代码中的常量值,因此在写入它之后,它仍将具有其初始值。
在您的测试中,您最终进入了第一种情况——对象(几乎可以肯定)是在堆栈上创建的,而堆栈不受写保护。如果对象是静态的,则可能会遇到第二种情况,如果启用更多优化,则可能会遇到第三种情况。
总的来说,编译器无法诊断此错误,除非目标引用或指针是常量还是非常量,否则无法判断(除了在您这样的非常简单的示例中)。你需要确保只有在对象不是常量或者你实际上不会修改它时才使用const_cast。

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

一个对象最初是非const的,这个想法怎么样?就像@Nikos所详细描述的那样。 - jww

0

编译器可能会将const数据放置在只读内存部分以进行优化,试图修改此数据将导致未定义行为。


0

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接