类层次结构可以安全且易于复制吗?

时间:2015-05-06 16:46:45

标签: c++

我们在某些功能中使用依赖于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;
}

没有虚析构函数的类层次结构是一个问题,即使所有类都使用编译器的默认析构函数,这是不是正确的?因此,我不能拥有一个可以轻易复制的安全类层次结构吗?

5 个答案:

答案 0 :(得分:4)

此代码

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

将触发未定义的行为,因为§5.3.5/ p3声明:

  

在第一个替代(删除对象)中,如果要删除的对象的静态类型与其不同   动态类型,静态类型应该是要删除的对象的动态类型的基类   静态类型应具有虚拟析构函数或行为未定义

并且无论您的派生类中是否包含动态分配的对象(如果有的话,都非常糟糕),您不应该这样做。如果没有基类虚拟析构函数的类层次结构不是本身的问题,当您尝试将静态和动态类型与delete混合时,它就成了一个问题。

对派生类对象执行memcpy会给我带来不好的设计,我宁愿满足对“虚拟构造函数”的需求(即虚拟clone()函数在您的基类中)复制派生对象。

如果确保对象,子对象和基类可以轻易复制,则可以使类层次结构易于复制。如果您想阻止用户通过基类引用您的派生对象,您可以像Mark先建议的那样render the inheritance protected

class Header
{
public:
};

class CanData : protected Header
{               ^^^^^^^^^
public:
};

int main() {
  Header *pt = new CanData(); // <- not allowed
  delete pt;
}

请注意,由于§4.10/ p3 - 指针转换,您将无法使用基本指针来引用派生对象。

答案 1 :(得分:1)

如果删除指向作为基类型保存的派生类型的指针,并且您没有虚拟析构函数,则无法调用派生类型析构函数,无论它是否隐式生成或不。无论是否隐式生成,您都希望它被调用。如果派生类型的析构函数实际上不会做任何事情,它可能不会泄漏任何东西或导致问题。如果派生类型包含std::stringstd::vector或任何具有动态分配的内容,则需要调用dtors。作为一个好的实践,你总是想要一个基类的虚析构函数,无论是否需要调用派生类析构函数 (因为基类不应该知道从它派生的内容) ,它不应该做出这样的假设。)

如果您复制类似的类型:

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

您只会调用Base副本ctor。实际来自Derived的对象部分将不会涉及。 b2不会秘密地成为Derived,而只会是Base

答案 2 :(得分:1)

我的第一直觉是“不要这样做 - 找到另一种方式,一个不同的框架,或者修复框架”#34;。但是,为了好玩,我们假设您的班级副本并不以任何方式依赖于该类的复制构造函数或其被调用的任何组成部分。

然后,由于您明确继承而不是替代解决方案很容易:使用protected继承并解决您的问题,因为他们无法再多态访问或删除您的对象,从而阻止了未定义的行为。

答案 3 :(得分:0)

这几乎是安全的。

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

delete h调用Header的析构函数,然后释放h指向的内存。释放的内存量与最初在该内存地址分配的内存量相同,而不是sizeof(Header)。由于HeaderCanData是微不足道的,因此他们的析构函数什么都不做。

但是,必须提供虚拟析构函数,即使它什么也不做(根据标准的要求来避免未定义的行为)。一个共同的指导原则是基类的析构函数必须是公共的和虚拟的,或者是受保护的和非虚拟的

当然,你必须像往常一样小心切片。

答案 4 :(得分:0)

感谢大家发布各种建议。我尝试了一个总结性答案,并提供了解决方案的其他建议。

我的问题的先决条件是达到一个可以轻易复制的类层次结构。请参阅http://en.cppreference.com/w/cpp/concept/TriviallyCopyable,尤其是一个简单的析构函数(http://en.cppreference.com/w/cpp/language/destructor#Trivial_destructor)的要求。该类不需要实现析构函数。这限制了允许的数据成员,但对我来说没问题。该示例仅显示没有动态内存分配的C兼容类型。

有人指出我的代码问题是未定义的行为,不一定是内存泄漏。马可引用了这个标准。谢谢,真的很有帮助。

根据我对答案的理解,可能的解决方案如下。如果我错了,请纠正我。解决方案的要点是基类的实现必须避免可以调用它的析构函数。

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

class CanData : protected Header
{
  ...
};

它可以工作,但避免人们可以访问Header的公共接口。这是拥有基类的初衷。 CanData需要将这些函数转发给Header。在结果中,我会重新考虑在这里使用组合而不是继承。但解决方案应该有效。

解决方案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;
};

然后没有用户可以删除标题。这对我来说没问题,因为Header本身没有任何目的。通过公共派生,公共接口仍然可供用户使用。

我的理解是CanData不需要实现析构函数来调用基类的析构函数。所有都可以使用默认的析构函数。不过我对此并不完全确定。

总而言之,在原始假设结束时我的问题的答案是:

  1. 即使所有类都使用编译器的默认析构函数,没有虚析构函数的类层次结构是否正确?

    如果您的析构函数是公共的,那么这只是一个问题。除派生类外,您必须避免人们可以访问您的desctrutor。并且您必须确保派生类(隐含地)调用基类的析构函数。

  2. 因此,我不能拥有一个可以轻易复制的安全类层次结构吗?

    您可以使用受保护的继承或受保护的析构函数使您的基类安全。然后,您可以拥有一个简单的可复制类的层次结构。