类层次结构可以安全且琐碎地复制吗?

Can a class hierarchy be safe and trivially copyable?

本文关键字:复制 层次结构 安全      更新时间:2023-10-16

我们使用的框架依赖于某些功能中的memcpy。为了我的理解,我可以将所有可以在这些功能上毫无复制的东西提供给所有的东西。

现在我们要使用一个简单的类层次结构。我们不确定由于安全的破坏,我们是否可以拥有一类层次结构,从而导致可复制的类型。示例代码看起来像这样。

class Timestamp; //...
class Header
{
public:
  uint8_t Version() const;
  const Timestamp& StartTime();
  // ... more simple setters and getters with error checking
private:
  uint8_t m_Version;
  Timestamp m_StartTime;
};
class CanData : public Header
{
public:
  uint8_t Channel();
  // ... more setters and getters with error checking
private:
  uint8_t m_Channel;
};

基类用于几个类似的子类。在这里,我省略了所有构造函数和破坏者。因此,这些类是可以复制的。我想,尽管用户可以编写导致这样的内存泄漏的代码:

void f()
{
  Header* h = new CanData();
  delete h;
}

,即使所有类都使用编译器的默认破坏者,没有虚拟破坏者的类层次结构是一个问题吗?因此,我不能拥有一个可以毫无复制的安全类层次结构?

此代码

Header* h = new CanData();
delete h;

将触发未定义的行为自§5.3.5/p3陈述:

在第一个替代方案(删除对象)中,如果要删除的对象的静态类型与其不同 动态类型,静态类型应为要删除对象的动态类型的基类,并 静态类型应具有虚拟破坏者或行为不确定

,无论您的派生类中没有动态分配的对象(如果有),您都不应该这样做。没有基类虚拟驱动器的类层次结构不是问题本质上,当您尝试将静态和动态类型与delete混合在一起时,它会成为一个问题。

在派生的类对象上做memcpy对我来说是不良设计的气味,我宁愿解决对" 虚拟构造函数"的需求(即,在您的基类中的虚拟clone()功能)来复制您的派生对象。

,如果您确保对象,其子对象和基本类是可复制的。如果您想防止用户通过基本类引用您的派生对象,如马克所建议的那样

class Header
{
public:
};
class CanData : protected Header
{               ^^^^^^^^^
public:
};
int main() {
  Header *pt = new CanData(); // <- not allowed
  delete pt;
}

请注意,由于§4.10/p3 -Pointer Conversions。>如果您删除将派生类型视为基本类型的指针,并且您没有虚拟驱动器,则不会调用派生类型的驱动器,无论它是否隐含生成。无论是否隐含生成,您都希望将其称为。如果派生的类型的驱动器实际上不会做任何事情,那么它可能不会泄漏任何东西或引起问题。如果派生的类型保存类似于std::stringstd::vector或具有动态分配的任何内容,则希望调用DTOR。从好的实践中,您总是希望基本类的虚拟驱动器是否要调用派生类destructors 需要(因为基类都不知道从中衍生的是什么,不应该这样做的假设)。

如果您复制这样的类型:

Base* b1 = new Derived;
Base b2 = *b1;

您只会调用Base s复制CTOR。实际上来自Derived的对象的部分将不涉及。b2不会秘密地是Derived,它将只是Base

我的第一个本能是"不要这样做 - 找到另一种方法,不同的框架,或修复框架"。但是,只是为了好玩,让我们假设您的班级副本不取决于班级的复制构造函数或其所谓的任何零件。

那么,由于您显然是继承了实施而不是替代解决方案很容易:使用protected继承并解决了您的问题,因为它们无法再多键型访问或删除对象,从而阻止了未定义的行为。

几乎是安全的。特别是,

中没有内存泄漏
Header* h = new CanData();
delete h;

delete h调用Header的驱动器,然后释放由h指向的内存。释放的内存量与最初分配的内存地址相同,而不是sizeof(Header)。由于HeaderCanData是微不足道的,因此它们的攻击者无能为力。

但是,您也必须提供虚拟驱动器即使没有任何作用(根据标准要求避免不确定的行为)即使它无能为力)。一个常见的指南是,基础类的驱动器必须是公共和虚拟或受保护和非虚拟的

当然,您必须照常切片。

感谢大家发布各种建议。我尝试使用解决方案的其他建议来汇总答案。

我的问题的先决条件是达到一个可复制的类层次结构。请参阅http://en.cppreference.com/w/cpp/concept/trivallycopyable,尤其是琐碎的驱动器(http://en.cppreference.com/w/cpp/language/destructor#trivial_dstrestuctor)。该类不需要实施攻击方。这限制了允许的数据成员,但对我来说很好。该示例仅显示没有动态内存分配的C兼容类型。

有些人指出,我的代码问题是不确定的行为,不一定是内存泄漏。Marco引用了有关此的标准。谢谢,真的很有帮助。

从我对答案的理解中,可能的解决方案如下。如果我错了,请纠正我。解决方案的观点是,基类的实现必须避免可以调用其破坏者。

解决方案1:建议的解决方案使用受保护的继承。

class CanData : protected Header
{
  ...
};

它有效,但避免人们可以访问标头的公共接口。这是拥有基础类的最初意图。Candata需要将这些功能转发到标题。在后面,我会重新考虑在这里使用构图而不是继承。但是解决方案应该起作用。

解决方案2:必须保护标头的破坏者,而不是整个基类。

class Header
{
public:
  uint8_t Version() const;
  const Timestamp& StartTime();
  // ... more simple setters and getters with error checking
protected:
  ~Header() = default;
private:
  uint8_t m_Version;
  Timestamp m_StartTime;
};

那么,没有用户可以删除标头。这对我来说很好,因为标头独自没有目的。通过公共派生,公共界面仍然可用于用户。

我的理解是,Candata不需要实施攻击函数来调用基类的后裔。所有人都可以使用默认驱动器。我不完全确定这个。

总的来说,在原始提出的结尾,我问题的答案是:

  1. ,即使所有类都使用编译器的默认破坏者?

    如果您的灾难是公开的,这只是一个问题。您必须避免人们可以访问您的后裔,除了派生的课程。并且您必须确保(隐含地)派生的类调用(隐含地)基类的驱动器。

  2. 因此,我不能拥有可毫无复制的安全类层次结构吗?

    您可以通过受保护的继承或受保护的后裔使基类安全。然后,您可以拥有一个可复制的类的层次结构。