【问题标题】:inherit from a POD struct [closed]从POD结构继承[关闭]
【发布时间】:2021-11-09 10:09:25
【问题描述】:

我正在尝试为几个存储数据的类找到最佳设计。 据我所知,我可以用继承做一些事情:

struct Data
{
    int a;
    int b;
    int c;
};

struct DerivedData : public Data
{
    int d;
};

struct AnotherDerivedData : public Data
{
    int e;
};

或组合:

struct ComposedData
{
    Data data;
    int d;
};

struct AnotherComposedData
{
    Data data;
    int e;
};

我倾向于继承,因为 Data 和 DerivedData 之间存在关系。但是我想知道这是否是一个好的设计,因为绝对没有要继承的功能?

此外,数据不应该单独使用,所以我想知道是否可以将其抽象化,是否值得?我已经读过要做到这一点,你应该创建一个纯虚拟析构函数,或者最好是一个受保护的构造函数:

struct Data
{
    int a;
    int b;
    int c;

protected:
    Data() = default;
};

但我不喜欢这里的是,它在原本简单的结构中引入了一些复杂性。它甚至不是 POD(如果我正确理解 POD 的话)。

关于最佳实施的任何建议?还有其他方法吗?

编辑:我意识到我对主要困扰我的事情还不够清楚: DerivedData 和 Data 之间存在“is-a”关系。但由于某种原因,我觉得继承有点“过度”,因为没有要覆盖的函数(甚至根本没有函数)。但也许这是我的一个误解,继承在这里是完全合适的?

【问题讨论】:

  • “数据不应单独使用”:这是否意味着您的 Composed/DerivedData 的“消费者”也不应该能够处理 a、b 或 c?
  • 上下文太少,不能基于意见。 DataDerivedDataAnotherDerivedData 是什么? pod-ness 的相关性是什么? (一旦你有了一个基类,它就不是 pod,那么如果 Data 是 pod,为什么还要麻烦呢?)
  • 是的,在现代 C++ 中,普通的旧数据通常并不重要——有时其他事情也很重要,比如 const 可初始化,或默认可构造(或可复制构造、可移动……取决于上下文)
  • Data 代表 Engine 和 DerivedData Car,还是 Data Car 和 DerivedData Porshe?这真的取决于你在建模什么以及你想从中得到什么。
  • @MarcusMüller 我的意思是客户通常应该使用 Composed/DerivedData 而不仅仅是数据。数据包含一些必需但不充分的基本信息(至少对于我想做的事情,但我想也许我应该允许客户在他们愿意的情况下只使用数据)。客户端应该能够处理 a、b 或 c。

标签: c++


【解决方案1】:

API

AFAIU Data 对于客户来说应该是“私人的”。但是,组合方法让他们知道这一点 - 他们在继承情况下输入 ComposedData{}.data.a 而不是 ComposedData{}.a

安全和性能

数据不应该单独使用,所以我想知道是否可以将其抽象化,是否值得?我已经读过要做到这一点,你应该创建一个纯虚拟析构函数,或者最好是一个受保护的构造函数

虚拟析构函数通过虚拟表进行调度,在所描述的情况下,它只会给您带来开销。要禁止使用原始Data,只需将其析构函数对非派生者隐藏:

class Data {
protected: ~Data() = default;
public: int a, b, c;
};

struct ComposedData: public Data { int d; }; // has access to the protected ~Data()

int main() {
  Data raw; // doesn't compile: raw.~Data() is inaccessible
  ComposedData composed; // OK (~ComposedData() is public)
}

此外,受保护的析构函数与受保护的构造函数不同,可以防止类似这样的错误:

struct NonTrivialData: public Data { std::string non_trivial_dtor; };

std::unique_ptr<Data> type_erased{new NonTrivialData{1, 2, 3, "i won't be deallocated"}};
// ...because ~Data() knows nothing about NonTrivialData::non_trivial_dtor

(我没有使用通常的make_unique,因为它无法执行聚合初始化。)

受保护的析构函数不允许从外部调用 ~Data() => 代码无法编译。

虚拟析构函数在运行时将~Data() 调度为~NonTrivialData() => 正确完成了析构。然而,就使用原始Data 而言并不可取,在编译时防止错误并且不引入虚拟表可提供更好的语义和性能。

感觉不错

由于某种原因,我觉得继承有点“过度”,因为没有要覆盖的功能

没关系。例如。在某些元代码中,甚至可以合理地从类似的类继承

template<typename ValueType> struct AliasHelper {
  using value_type = ValueType;
  using reference = ValueType&;
  using pointer = ValueType*;
  using const_reference = ValueType const&;
  using const_pointer = ValueType const*;
};

class Foo: public AliasHelper<int> {};
class Bar: public AliasHelper<short> {};
class Baz: public AliasHelper<long> {};

它甚至没有字段!但是,它大大减少了样板。

【讨论】:

  • Re:继承的开销:同样值得注意的是,除了虚拟方法调用之外,基本上任何东西都没有额外的开销。继承是一种语言结构;当您访问derivedclass.a 时,编译器会在编译时知道a 的位置,这与组合情况完全相同,或者您不执行任何操作而只复制并粘贴相同字段的情况。
  • @MarcusMüller 只是为了完整性,与将所有字段放在同一位置相比,POD 结构继承的一个小缺点是它不保证成员的布局,也不保证关于根据这篇文章填充:stackoverflow.com/questions/22404423/…。我不知道它是否也适用于构图,我认为布局部分不会。无论如何,这似乎是一个非常小的问题,但由于我在问我的问题之前偶然发现了这个线程,所以我想我会提到它。
  • 我觉得还不错吧?因为该派生对象的地址是第一个非静态数据成员的地址已经很好地定义了,并且因为我们知道向下转换可以在不改变地址的情况下工作,所以它直接遵循“第一个”基类的第一个元素需要先来吧?
  • 不,我认为这不是很重要,否则我猜继承会有很多问题,不仅仅是 POD。我不是内存布局方面的专家,但大多数编译器似乎都按照您的预期实现它。你的评论让我想起了这篇文章,这就是我链接它的原因,也供将来参考
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多