这与这里有关类型擦除的问题有关: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++ 中是否合法,或者是未定义的行为?
为什么这似乎在 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();
}