3

考虑以下示例:

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)上,它按预期工作(打印“Hi!”)。有谁知道配置此代码可能无法正常工作?或者可能是具有相同想法的更复杂的示例(请注意,Derived 无论如何都不应该携带任何额外的数据)?

更新:

从标准 5.2.9/8 开始:

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

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

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

谢谢,迈克。

4

6 回答 6

9

该函数fun()实际上并没有做任何与指针有关的事情this,并且由于它不是一个虚函数,因此查找该函数没有什么特别需要的。基本上,它像任何普通(非成员)函数一样被调用,但this指针错误。它只是不会崩溃,这是完全有效的未定义行为(如果这不是矛盾的话)。

于 2012-04-23T09:51:19.933 回答
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()相当良性,因此在这种情况下,简单的方法会产生相当良性的结果。

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

于 2012-04-23T11:37:27.017 回答
3

在同一台机器上无限次运行相同的代码,如果幸运的话,也许你会看到它运行不正确并且出乎意料。

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

于 2012-04-23T09:51:14.910 回答
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_ 应该始终为假。假设这一点,编译器可能会将整个代码删除为不可访问。

[编辑]编译器如何消除“服务”UB 案例的代码的真实示例:

为什么使用 GCC 在 x86 上整数溢出会导致无限循环?

于 2012-04-23T13:37:13.690 回答
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 或其他一些完全不相关的类。 行为是未定义的,但这里没有什么可以出错的。

//编辑:好的,根据标准,情况不相等。((Derived*)NULL)->fun(); 被明确声明为 UB。但是,此行为通常在有关调用约定的编译器文档中定义。我应该写“对于我所知道的所有编译器,什么都不会出错”。

于 2012-04-23T10:38:38.880 回答
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(). 如您所见,考虑到这种情况,这两种优化都是相当合理的。

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

于 2012-04-23T14:06:21.943 回答