C ++编译器如何优化错误的分层向下转换以导致真正的未定义行为

时间:2012-04-23 09:47:17

标签: c++ compiler-optimization undefined-behavior

考虑以下示例:

class Base {
public:
    int data_;
};

class Derived : public Base {
public:
    void fun() { ::std::cout << "Hi, I'm " << this << ::std::endl; }
};

int main() {
    Base base;
    Derived *derived = static_cast<Derived*>(&base); // Undefined behavior!

    derived->fun(); 

    return 0;
}

根据C ++标准,函数调用显然是未定义的行为。但是在所有可用的机器和编译器(VC2005 / 2008,RH Linux和SunOS上的gcc)上,它按预期工作(打印“嗨!”)。有谁知道配置此代码可以正常工作?或者可能是具有相同想法的更复杂的例子(注意,Derived不应该携带任何额外的数据)?

更新

从标准5.2.9 / 8:

  

“指向cv1 B的指针”类型的右值,其中B是类类型,可以是   转换为“指向cv2 D的指针”的右值,其中D是a   来自B的类派生(第10条),如果是有效的标准转换   “指向D的指针”到“指向B的指针”存在(4.10),cv2是相同的   cvqualification as或更高的cvqualification,cv1和B不是   D的虚基类。转换空指针值(4.10)   到目标类型的空指针值。 如果是rvalue   键入“指向cv1 B的指针”指向实际上是其子对象的B.   D类型的对象,结果指针指向封闭   类型为D.的对象。否则,未定义强制转换的结果。

还有一个9.3.1(感谢@Agent_L):

  

如果为对象调用类X的非静态成员函数   不是X类型,或者是从X派生的类型,行为是   未定义。

谢谢, 麦克

6 个答案:

答案 0 :(得分:9)

函数fun()实际上并没有做任何与this指针有关的事情,因为它不是虚函数,所以查找函数没有什么特别之处。基本上,它被称为任何普通(非成员)函数,带有错误的this指针。它只是没有崩溃,这是完全有效的未定义行为(如果这不是一个矛盾)。

答案 1 :(得分:5)

对代码的评论不正确。

Derived *derived = static_cast<Derived*>(&base);
derived->fun(); // Undefined behavior!

更正版本:

Derived *derived = static_cast<Derived*>(&base);  // Undefined behavior!
derived->fun(); // Uses result of undefined behavior

未定义的行为以static_cast开头。此恶意指针的任何后续使用也是未定义的行为。未定义的行为是为编译器供应商提供的无监狱卡。几乎所有编译器的响应都符合标准。

没有什么可以阻止编译器拒绝你的演员阵容。一个好的编译器可能会为static_cast发出致命的编译错误。在这种情况下,很容易看到违规。一般来说,这并不容易看到,因此大多数编译器都不会费心检查。

大多数编译器都采用最简单的方法。在这种情况下,简单的方法是简单地假装指向类Base的实例的指针是指向类Derived的实例的指针。由于您的函数Derived::fun()非常温和,因此在这种情况下的简单方法会产生相当良性的结果。

仅仅因为你获得了良好的良性结果并不意味着一切都很酷。它仍然是未定义的行为。最好的办法是永远不要依赖未定义的行为。

答案 2 :(得分:3)

在同一台计算机上无限次运行相同的代码,可能如果你是错误 幸运。

要理解的是,未定义的行为(UB)并不意味着它肯定不能按预期运行; 可能按预期运行,1次,2次,10次,甚至无限次。 UB只是意味着它不是保证按预期运行。

答案 3 :(得分:1)

你必须了解你的代码正在做什么,然后你可以看到它没有做错。 “this”是由编译器为您生成的隐藏指针。

class Base
{
public:
    int data_;
};

class Derived : public Base
{

};


void fun(Derived* pThis) 
{
::std::cout << "Hi, I'm " << pThis << ::std::endl; 
}

//because you're JUST getting numerical value of a pointer, it can be same as:
void fun(void* pThis) 
{
    ::std::cout << "Hi, I'm " << pThis << ::std::endl; 
}

//but hey, even this is still same:
void fun(unsigned int pThis) 
{
    ::std::cout << "Hi, I'm " << pThis << ::std::endl; 
}

现在很明显:这个功能不会失败。您甚至可以传递NULL或其他完全不相关的类。 行为未定义,但这里没有任何问题可以解决。

//编辑:好的,根据标准,情况并不相同。 ((导出*)NULL) - &GT;乐趣();显式声明为UB。但是,此行为通常在有关调用约定的编译器文档中定义。 我应该写“对于我知道的所有编译器,没有什么可以出错。”

答案 4 :(得分:1)

例如,编译器可以优化代码。 考虑一下不同的程序:

if(some_very_complex_condition)
{
  // here is your original snippet:

  Base base;
  Derived *derived = static_cast<Derived*>(&base); // Undefined behavior!

  derived->fun(); 
}

编译器可以

(1)检测未定义的行为

(2)假设程序不应暴露未定义的行为

因此(编译器决定)_some_very_complex_condition_应始终为false。假设这样,编译器可能会将整个代码消除为无法访问。

[edit] 一个真实世界的例子,编译器如何消除“服务”UB案例的代码:

Why does integer overflow on x86 with GCC cause an infinite loop?

答案 5 :(得分:1)

此代码经常起作用的实际原因是任何打破此问题的方法都倾向于在发布/优化性能构建中进行优化。但是,任何专注于查找错误的编译器设置(例如调试版本)都更有可能在此处跳闸。

在这些情况下,您的假设(“请注意,Derived不应该携带任何其他数据”)并不成立。它绝对应该,以方便调试。

一个稍微复杂的例子甚至更棘手:

class Base {
public:
    int data_;
    virtual void bar() { std::cout << "Base\n"; }
};

class Derived : public Base {
public:
    void fun() { ::std::cout << "Hi, I'm " << this << ::std::endl; }
    virtual void bar() { std::cout << "Derived\n"; }
};

int main() {
    Base base;
    Derived *derived = static_cast<Derived*>(&base); // Undefined behavior!

    derived->fun(); 
    derived->bar();
}

现在合理的编译器可能决定跳过vtable并静态调用Base::bar(),因为那是你正在调用bar()的对象。或者它可能会决定derived必须指向真实Derived,因为您在其上调用fun,跳过vtable,然后调用Derived::bar()。如您所见,根据具体情况,两种优化都是非常合理的。

在这里我们看到为什么Undefined Behavior会如此令人惊讶:编译器可以在使用UB的代码之后做出错误的假设,即使语句本身编译正确。