C++多接口继承和static_cast

C++ multiple interfaces inheritance and static_cast

在下面的代码中:

Live

#include <iostream>
#include <thread>
#include <mutex>
#include <functional>

struct IView {
    virtual void setOnClick() = 0;
};
struct ITextView : IView {
    virtual void setText() = 0;
};
struct IButton : ITextView {
    virtual void setRadius() = 0;
};

struct View : IView {
    int i = 1;
    virtual void setOnClick() override {
        std::cout << "setting OnClick! i: " << i << std::endl;
    };
};

/// Works as is
/// But if make "TextView : View, ITextView" - have segfault on the run
struct TextView : ITextView, View {
    int j = 2;
    virtual void setText() override {
        std::cout << "setting text! i: " << i << " j: " << j << std::endl;
    };

    // forward IView
    virtual void setOnClick() override {
        View::setOnClick();
    }
};


int main() {
    TextView tv;

    void* ptr = &tv;    // I need to pass raw pointer, and then restore "interface" from it

    ITextView* itv = static_cast<ITextView*>(ptr);  // I don't need safety checks here
    itv->setOnClick();
    itv->setText();

    return 0;
}

如果我更改 TextView 的继承顺序,我将在 itv->setText(); 调用时出现段错误。

为什么重要?我这里可以用static_cast,还是我这里有UB?据我了解,dynamic_cast 只需要虚拟继承,而据我所知,情况并非如此。

指向不同class"slices"的指针是不同的。

所以当你通过void *传输时,你需要

void * ptr = static_cast<ITextView *>(&tv);
...
ITextView* itv = static_cast<ITextView*>(ptr)

请注意,您的基础 class 层次结构中有两个 "instances" IView:

TextView  <- ITextView <- ... <- IView
          \- View <- IView

有虚拟继承把它变成"diamond":wikipedia

就目前而言,您从 TextView* 隐式转换为 void*,然后从 void* 显式转换为 ITextView*。这些转换在转换 from/to void* 时不执行任何指针调整,因此您最终得到一个 ITextView* 类型的指针,它实际上指向 TextView(而不是它的 ITextView 子对象!):未定义的行为随之而来。

解决方案是 始终void*:

的两个 "sides" 上使用完全相同的类型
TextView tv;

void* ptr = static_cast<ITextView*>(&tv); // Adjust, then convert to void*

ITextView* itv = static_cast<ITextView*>(ptr);

重点是您正在对通过 void* 向编译器隐藏实际类型的不同类型执行 static_cast

我的意思是:

TextView tv;
ITextView* itv = static_cast<ITextView*>(&tv);

这是安全的,您正在显式转换为父类型。

TextView tv;
void* ptr = &tv;
ITextView* itv = static_cast<ITextView*>(ptr);

这里编译器不知道 ptr 的实际类型并将其视为 ITextView*,因此它无法将指针调整到对象的正确子部分(它是值得注意的是,这似乎可行,因为 ITextView 没有任何成员变量)。这与继承是如何在引擎盖下实现的有关,想想:

---------------------
| VTable A | Type A |
---------------------

---------------------
| VTable B | Type B |
---------------------

-----------------------------------------
| VTable AB | Type A | Type B | Type AB |
-----------------------------------------

假设这是

的内存布局
class A { /* members */ };
class B { /* members */ };
class AB : public A, public B { /* members */ };

现在很明显,如果你有一个 AB 的地址并且你将它转换为一个 B* 而没有让编译器知道它是一个 AB 那么编译器无法指向正确的 Type B 部分,该部分位于 Type A 切片之后。只有首先知道它是 AB 才能知道这一点。