【问题标题】:Can a class hierarchy be safe and trivially copyable?类层次结构可以安全且易于复制吗?
【发布时间】:2015-07-16 22:46:35
【问题描述】:

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

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

【问题讨论】:

  • 仅供参考,f() 的代码不应导致内存泄漏,因为delete 会知道h 指向CanData 的实例。
  • @JoeSewell:实际上,由于缺少虚拟析构函数,它有UB。
  • @Lightness,即使根本没有虚函数,因此类是非多态的?
  • @SergeyTachenov:是的。阅读[C++11: 5.3.5/3]
  • 哦,我明白了。事实上,即使是一个微不足道的析构函数也不是那么微不足道 - 它可以调用字段的析构函数,它可以做任何事情,因此将任何可能不调用析构函数(微不足道或不重要)的情况定义为 UB 实际上是有意义的。跨度>

标签: c++


【解决方案1】:

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

如果你像这样复制一个类型:

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

您只会调用Bases 复制ctor。对象中实际来自Derived 的部分将不涉及。 b2 不会偷偷变成 Derived,只会是 Base

【讨论】:

  • 如果你删除一个指向作为其基类型的派生类型的指针并且你没有虚拟析构函数,派生类型析构函数将不会被调用它是未定义的行为,所以任何事情都可能发生。你无法分辨什么析构函数被调用,什么不被调用。
【解决方案2】:

这几乎是安全的。特别是在

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

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

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

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

【讨论】:

  • “缺少虚拟析构函数不是问题” 除了具有未定义的行为,因此可以做任何事情([C++11: 5.3.5/3])。跨度>
  • @LightnessRacesinOrbit 感谢您指出这一点(我已相应地调整了答案)。我没有完全意识到这一点。大多数编译器只是简单地警告这一点,但通常不会在基类中有一个公共的非虚拟析构函数是错误的。那么这背后的逻辑是什么?
  • @Walter:delete 通过基指针派生的类型只是未定义的行为,编译器几乎不可能检测到。
  • @Walter 编译器并非无所不知,他们无法预测未来 :)
【解决方案3】:

这段代码

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 - 指针转换,您根本无法使用基指针来引用派生对象。

【讨论】:

    【解决方案4】:

    我的第一直觉是“不要那样做 - 寻找另一种方式,不同的框架,或修复框架”。但只是为了好玩,我们假设您的类副本肯定不依赖于类的复制构造函数或其任何被调用的组成部分。

    那么既然你显然是继承来实现而不是替代解决方案很容易:使用protected继承,你的问题就解决了,因为它们不能再多态地访问或删除你的对象,从而防止未定义的行为。

    【讨论】:

    • @MarcoA 父级是完全非多态的,所以有一个指向它的指针只会导致问题。
    【解决方案5】:

    感谢大家发布各种建议。我尝试用一​​个额外的解决方案来总结答案。

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

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

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

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

    class CanData : protected Header
    {
      ...
    };
    

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

    解决方案2: Header 的析构函数必须被保护,而不是整个基类。

    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。这对我来说很好,因为 Header 本身没有任何目的。通过公共派生,公共接口仍然可供用户使用。

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

    总而言之,我在原文结尾的问题的答案是:

    1. 即使所有类都使用编译器的默认析构函数,没有虚析构函数的类层次结构是不是也有问题?

      只有当你的析构函数是公开的时才会有问题。您必须避免人们可以访问您的 desctrutor,派生类除外。而且您必须确保派生类(隐式)调用基类的析构函数。

    2. 因此,我不能拥有一个可轻松复制的安全类层次结构是否正确?

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-04-22
      • 1970-01-01
      • 2018-06-19
      • 2023-03-20
      • 1970-01-01
      • 1970-01-01
      • 2017-12-03
      相关资源
      最近更新 更多