【问题标题】:XY inherits from both X and Y. casting XY* to X* then to Y* then calling Y's function results in calling X's functionXY 继承自 X 和 Y。将 XY* 转换为 X* 然后转换为 Y* 然后调用 Y 的函数会导致调用 X 的函数
【发布时间】:2020-08-16 06:09:22
【问题描述】:
#include <iostream>

struct X
{
    virtual void x() = 0;
};

struct Y
{
    virtual void y() = 0;
};

struct XY : X, Y
{
    void x() override { std::cout << "X\n"; }
    void y() override { std::cout << "Y\n"; }
};

int main()
{
    XY xy;

    X* xptr = &xy;

    Y* yptr = (Y*)xptr;

    yptr->y(); //prints "X"....

    ((Y*)((X*)(&xy)))->y(); // prints "Y"....
}

输出:

X
Y

有人能详细解释一下为什么会这样吗?为什么第一个调用打印 X 以及为什么两个调用彼此不同?

【问题讨论】:

  • Y* yptr = (Y*)xptr; 这个演员表实际上是在做一个reinterprest_cast,它只是强制转换。这不合法,也不安全。事实上,这是未定义的行为。您可以安全地将xy 隐式转换为yptr
  • 对我来说似乎是未定义的 bahviour。您在不相关的类型之间进行转换。如果您使用 dynamic_cast,它可能会起作用?
  • reinterpret_cast 之后,你仍在调用指向X 的指针,因此它调用了X 的函数。即使是 UB 也没什么好奇怪的
  • @user,不,这是不正确的。它将 X 的 vtable 解释为好像它是 Y vtable。碰巧它们是“兼容的”,因为它们具有相同数量的具有相同签名的函数。
  • 这说明了为什么你不应该 C 风格强制转换为指针或引用。

标签: c++ casting multiple-inheritance virtual-functions


【解决方案1】:

如 cmets 中所述,就语言而言,这是未定义的行为。

但是,实际选择的行为确实揭示了典型 C++ 编译器的内部工作原理,因此调查为什么得到这样的输出仍然很有趣。话虽如此,重要的是要记住以下解释并不普遍。以这种方式工作没有硬性要求,任何依赖于这种行为的代码都会被有效地破坏,即使它适用于您尝试的所有编译器

C++ 多态性通常使用 vtable 来实现,它基本上是一个函数指针列表,可以看作是对象中的隐藏成员指针。

所以

struct X
{
    virtual void x() = 0;
};

struct Y {
    virtual void y() = 0;
};

大致相当于(实际上并没有使用std::function&lt;&gt;,但这使得伪代码更易读):

struct X {
    struct vtable_t {
      std::function<void(void*)> first_virtual_function;
    };
    
    vtable_t* vtable;

    void x() {
      vtable->first_virtual_function(this);
    }
};

struct Y {
    struct vtable_t {
      std::function<void(void*)> first_virtual_function;
    };
    
    vtable_t* vtable;

    void y() {
      vtable->first_virtual_function(this);
    }
};

注意X::vtable_tY::vtable_t 巧合本质上是相同的。如果XY 有不同的虚函数,事情就不会这么整齐了。

另一个重要的难题是多重继承实际上是一种串联:

struct XY : X, Y {
    void x() override { std::cout << "X\n"; }
    void y() override { std::cout << "Y\n"; }
};

// is roughly equivalent to:
struct XY {
  static X::vtable vtable_for_x; // with first_virtual_function assigned to XY::x()
  static Y::vtable vtable_for_y; // with first_virtual_function assigned to XY::y()

  X x_base;
  Y y_base;

  XY() {
    x_base.v_table = &vtable_for_x;
    y_base.v_table = &vtable_for_y;
  }

  void x() { std::cout << "X\n"; }
  void y() { std::cout << "Y\n"; }
};

这意味着从多重继承类型转换为基类型不仅仅是改变指针类型的问题,也必须改变。

只有X指针等价于基对象指针,Y指针实际上是一个不同的地址

X* xptr = &xy;  
// is equivalent to
X* xptr = &xy->x_base;

Y* xptr = &xy;  
// is equivalent to
Y* xptr = &xy->y_base;

最后,当您从X 转换为Y 时,由于这些类型不相关,因此操作是reinterpret_cast,因此虽然指针可能是指向Y 的指针,但底层对象仍然是X.

你很幸运,一切顺利:

  • X 和 Y 都将 vtable 指针作为第一个成员对象。
  • X和Y的vtable实际上是等价的,前者指向XY::x(),后者指向XY::y()

因此,当调用y() 的逻辑应用于X 类型的对象时,这些位恰好排成一列以调用XY::x()

【讨论】:

    【解决方案2】:

    Y* yptr = (Y*)xptr; 执行reinterpret_cast

    来自Explicit type conversion( new_type ) expression

    当遇到 C 风格的转换表达式时,编译器会尝试将其解释为以下转换表达式,顺序如下:

    a) const_cast&lt;new_type&gt;(expression);
    b) static_cast&lt;new_type&gt;(expression),带有扩展:即使基类不可访问(也就是说,此转换忽略了私有继承说明符)。同样适用于将指向成员的指针转换为指向明确非虚基成员的指针;
    c) static_cast(带扩展名)后跟const_cast;
    d) reinterpret_cast&lt;new_type&gt;(expression);
    e) reinterpret_cast 后跟 const_cast

    选择满足各个强制转换运算符要求的第一选择,即使不能编译

    abc 将不起作用,因此它会落在 d 上。 当您进行 C 样式转换时,甚至不会考虑正确的转换 dynamic_cast,因此您仍然有一个指向 XYX 部分的指针,当您通过 Y 的眼睛取消引用时你做yptr-&gt;y()。这使您的程序拥有undefined behavior

    永远不要使用 C 风格的强制转换。最好是明确的,这样你就知道你得到了正确的演员:

    Y* yptr = dynamic_cast<Y*>(xptr);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-03-06
      • 2021-11-01
      • 2015-10-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-04-14
      • 1970-01-01
      相关资源
      最近更新 更多