“reinterperet_cast”到非虚拟对象的子对象是否合法?

问题描述 投票:0回答:1

这与这里有关类型擦除的问题有关:boost te memory access failure with Visual C++。如果您认为可以更好地表达这一点,请随意编辑标题。

在类型擦除库中,执行了一个本质上类似于以下内容的技巧:

struct implementer {
  void func(){};
};

struct base {
  virtual void f() const = 0;
};

struct poly : public implementer, public base {
    void f() const {}
};

template <typename T>
void call(const T& v) {
    const auto& b= reinterpret_cast<const base &>(v);
    b.f();
}

int main() {
    poly p;
    implementer &i = p;
    call(i);
}

这里的关键是,因为您将实现者传递给调用函数,所以必须使用

reinterperet_cast
来取出多边形部分并检索已擦除的函数。这会在 gcc 和 clang 中编译并运行(正确获取基本 vtable)。

https://godbolt.org/z/os9W71av6

它也在 MSVC 中编译,但是当我运行它时,

reinterperet_cast
破坏了值
b
的 vtable,导致读取访问冲突:

抛出异常:读取访问冲突。 b 是 0xFFFFFFFFFFFFFFFF。

那么问题是 这种类型的强制转换在 C++ 中是否合法,或者是未定义的行为?

c++ gcc visual-c++ undefined-behavior
1个回答
0
投票

为什么这似乎在 Linux 上的 GCC/Clang 上“有效”:

implementer
经过空基类优化,放置在
poly
的前零字节中。接下来的 8 个字节存储 vtable 指针和
base
的所有零成员。

这意味着

p
static_cast<implementer&>(p)
(
i
) 和
static_cast<base&>(p)
都具有相同的地址。

你仍然有 UB,因为

reinterpret_cast
没有指向
base
对象,但这可以通过
std::launder
修复:

    const auto& b = *std::launder(&reinterpret_cast<const base &>(v));
    b.f();  // No longer UB

为什么这在带有 Microsoft ABI 的 MSVC/Clang 上不起作用:

由于

poly
不是标准布局,因此允许重新排列成员。至关重要的是,不能保证
implementer
在开始时存储在零字节中。 Microsoft ABI 为 vtable 指针 /
base
存储 8 个字节,然后为
base
存储 0 个字节。

这意味着

&i != &p
(多了 8 个字节,
&i == reinterpret_cast<char*>(p) + sizeof(void*)
)。

所以这意味着您的

reinterpret_cast
位于错误的地址。在 Windows 上您必须再次调整指针:

    const auto& b = *std::launder(reinterpret_cast<const base *>(&v) - 1);
    b.f();

根据班级的具体布局,您需要调整的数量显然会有所不同。


你所做的本质上是一种旁白。如果您将

implementer
设为虚拟,则可以使用
dynamic_cast
:

来完成此操作
struct implementer {
  void func() {}
  virtual ~implementer() = default;
};

// ...

template <typename T>
void call(const T& v) {
    const auto& b = dynamic_cast<const base &>(v);
    b.f();
}
© www.soinside.com 2019 - 2024. All rights reserved.