在我正在开发的一个库中,其目的是包装 C 建模库,我有一个类型
EntityBase
,它用作其他类(如 Body
、Face
等)的基类...:
class EntityBase
{
public:
// default ctor
EntityBase() = default;
// constructing from native handle
explicit constexpr EntityBase(int handle) noexcept
: m_handle{handle}{}
[[nodiscard]] constexpr int handle() const noexcept { return m_handle; }
// ... operations common among different entity types ...
protected:
// protected destructor to prevent deletion from pointer to base
~EntityBase() = default;
private:
int m_handle{0};
};
class Face : public EntityBase
{
public:
Face() = default;
explicit constexpr Face(int handle) noexcept
: EntityBase{handle}{}
// ... operations specific for a face ...
// IMPORTANT: no additional member is added to the Face class (and will never be by design)
};
// ... other classes (Body, Edge, etc...) ...
一个重要的信息是
EntityBase
及其派生类具有相同的大小,即 EntityBase
所包装的整数句柄的大小。
这是因为,根据设计,我希望这些代表强类型句柄的类型被传递到底层 C 库,从而强制类型安全,但没有开销。
因此,每种类型都有其语义(主体、面、边缘等),但某些操作是通用的(这就是基类存在的原因)。
EntityBase
not有一个virtual析构函数,以避免vtable的开销,并维护库类型的值语义。
因此,析构函数被声明为 protected 以防止有人从指向
EntityBase
的指针中删除派生类。
我发现,奇怪的是,MSVC 允许直接实例化
EntityBase
的实例:
consteval int test() noexcept
{
EntityBase e{42};
return e.handle();
}
int main()
{
Face f{ 42 };
EntityBase eb1; // compiles on MSVC
EntityBase eb2{ 1 }; // compiles on MSVC
EntityBase eb3{ f }; // compiles on MSVC
constexpr int value = test();
return eb1.handle() + eb2.handle() + eb3.handle() + value; // returns 85 on MSVC
}
相同的代码反而被 GCC 和 Clang 拒绝。
<source>: In function 'int main()':
<source>:40:16: error: 'constexpr EntityBase::~EntityBase()' is protected within this context
40 | EntityBase eb1;
| ^~~
<source>:19:5: note: declared protected here
19 | ~EntityBase() = default;
| ^
<source>:41:23: error: 'constexpr EntityBase::~EntityBase()' is protected within this context
41 | EntityBase eb2{ 1 };
| ^
<source>:19:5: note: declared protected here
19 | ~EntityBase() = default;
| ^
<source>:42:23: error: 'constexpr EntityBase::~EntityBase()' is protected within this context
42 | EntityBase eb3{ f };
| ^
<source>:19:5: note: declared protected here
19 | ~EntityBase() = default;
| ^
Compiler returned: 1
请注意,如果我尝试从指向
Face
的指针中删除 EntityBase
,MSVC(正确地)不会编译。此外,一旦我向派生类添加数据成员,MSVC 就开始拒绝 EntityBase
的直接实例化。
看来,当且仅当基类型与派生类型具有相同的大小时,MSVC 才允许直接实例化基类型,即使它具有受保护的析构函数。 此外,MSVC 甚至在
EntityBase
函数中也允许实例化 consteval
。
因此我的问题是:
Face
没有定义任何其他数据成员并且它是可轻易破坏的,那么通过指向 EntityBase
的指针删除 Face
仍然是 UB 吗?感谢任何试图澄清这种情况的人。
代码位于编译器资源管理器上:https://godbolt.org/z/njfPT5aqT。
这里谁是正确的:MSVC 还是 Clang/GCC?
MSVC显然不符合标准。
如果 Face 没有定义任何其他数据成员并且它是可轻易破坏的,那么通过指向 EntityBase 的指针删除 Face 仍然是 UB 吗?
是的,唯一重要的是基础析构函数是否是
virtual
。它不在这里,所以它是UB。