联合体作为基类

40
标准规定,联合体不能用作基类,但这是否有具体的原因?据我所知,联合体可以有构造函数、析构函数、成员变量以及操作这些变量的方法。简而言之,联合体可以封装数据类型和状态,并通过成员函数进行访问。因此,在通常情况下,它可以作为一个类的资格,并且如果它可以作为类的话,那么为什么会被限制不能作为基类呢?
编辑:虽然回答试图解释其原因,但我仍不明白将Union作为派生类比将Union作为类更糟糕的原因。所以,希望获得更具体的答案和原因,我将对此进行悬赏。对已经发布的答案没有冒犯之意,谢谢!

8
每次我看到这样疯狂但非常好的问题,我就意识到有了 Stack Overflow,这个世界变得更好了。 - sharptooth
1
@Armen:已经有一个标签 [language-lawyer] 用于此目的。 - Prasoon Saurav
1
@sharptooth: :) 这只是因为SO上优秀答案的质量设定了非常高的基准,鼓励像我这样的怪人去尝试理解语言的细节,从而产生类似这样疯狂但有趣的问题。 - Alok Save
1
可能是https://dev59.com/GHA65IYBdhLWcg3wzyP8...的重复问题。 - Tony Delroy
1
@L.F. 实际上,我并不在乎这个问题是否被“关闭”,我更关心它们之间的链接是否已经安装... - Aconcagua
显示剩余5条评论
7个回答

16

Tony Park提供了一个非常接近真相的答案。C++委员会基本上认为让联合成为C++的强大组成部分不值得付出努力,类似于将数组视为我们必须从C继承但实际上并不想要的遗留物。

联合存在问题:如果我们允许在联合中使用非POD类型,它们如何被构造?当然可以做到,但不一定安全,并且任何考虑都需要委员会资源。最终结果将不尽如人意,因为在理智的语言中真正需要的是辨别联合,而裸的C联合永远无法以与C兼容的方式被提升为辨别联合(至少我能想象到的)。

对于技术问题的详细说明:由于您可以在不失去任何东西的情况下用结构体包装一个仅限于POD成分的联合,因此允许联合作为基础没有任何优势。对于仅限于POD成分的联合组件,显式构造函数只需分配其中一个成分即可,使用位块传输(memcpy)作为编译器生成的复制构造函数(或赋值)也没有问题。

然而,这些联合除了保留它们以便现有的C代码可以被视为有效的C++之外没有足够的用处。这些仅限于POD成分的联合在C ++中是有问题的,因为它们未能保留在C中所具有的重要不变量:任何数据类型都可以用作组件类型。

要使联合有用,我们必须允许构造类型作为成员。这很重要,因为仅在构造函数体本身或任何封闭结构体中分配组件是不可接受的:例如,您无法将字符串分配给未初始化字符串组件。

因此,必须想出一些规则来使用mem-initializer初始化联合组件,例如:

union X { string a; string b; X(string q) : a(q) {} };

现在的问题是:什么是规则?通常,规则是您必须初始化类的每个成员和基类。如果您没有明确地这样做,则默认构造函数将用于其余部分,并且如果未明确初始化任何一个类型,则不具有默认构造函数,则会出现错误[异常:复制构造函数,默认是成员复制构造函数]。

很明显,这个规则对于联合体无法起作用: 规则必须改为:如果联合体至少有一个非POD成员,则必须在构造函数中明确初始化一个成员。在这种情况下,不会生成默认构造函数、复制构造函数、赋值运算符或析构函数,如果这些成员中的任何一个实际上被使用,则必须明确提供它们。

那么现在的问题是:您该如何编写例如复制构造函数?当然,如果您像X窗口事件联合一样设计联合,每个组件中都有辨别标记,则很可能做到并正确实现,但您必须使用放置new操作符来执行此操作,并且您必须打破我上面写的规则——乍一看似乎是正确的规则!

默认构造函数怎么办?如果没有这样的函数,就不能声明未初始化的变量。

还有其他情况下,您可以在外部确定组件并使用放置new来管理联合体,但那不是复制构造函数。事实上,如果您有N个成员,则需要N个构造函数,并且C++存在一个错误的想法,即构造函数使用类名,这会让您缺少名称,并强制您使用幻影类型以允许重载选择正确的构造函数...而对于复制构造函数,由于其签名是固定的,因此无法这样做。

那么,有替代方法吗?可能是的,但不容易想出,而且更难说服100多人在三天密集的会议中考虑它们,因为会议中还涉及其他问题。

遗憾的是委员会没有实施上述规则: 联合体是用于对齐任意数据的强制性工具,手动进行组件的外部管理并不是真的很难,当代码由适当的算法生成时,其是微不足道且完全安全的。换句话说,如果要将C++用作编译器目标语言并仍然生成可读性好、可移植的代码,则该规则是必需的。具有可构造成员的这些联合体具有许多用途,但最重要的用途是表示包含嵌套块的函数的堆栈帧:每个块在结构中具有局部数据,每个结构都是联合组件,不需要任何构造函数或类似的东西,编译器将只使用放置new。联合提供了对齐和大小,以及无需强制转换即可访问组件。

因此,对于您的问题的答案是:您问错了问题。POD-only联合体作为基类没有优势,它们当然不能是派生类,因为那样它们就不会是PODs。要使它们有用,需要一些时间来理解为什么应该遵循C++中其他地方使用的原则:除非尝试使用缺失的位,否则不存在错误。


9

联合体是一种类型,可以根据已设置的成员使用其任何一个成员 - 只能稍后读取该成员。

当您从一个类型派生时,派生类型继承基本类型 - 派生类型可以在基本类型可以使用的任何地方使用。如果您可以从联合体派生,则派生类可以在任何联合体成员可以使用的地方使用(不是隐式地,而是通过命名成员显式使用),但在这些成员中只有一个成员可以合法访问。问题是哪个成员已设置的数据未存储在联合体中。

为避免这种微妙但危险的矛盾实际上颠覆了类型系统,不允许从联合体派生。


5
我不认为我理解这个论点。当然,只有一个联合成员可访问,但这如何使派生类比基类“更差”呢?因为这也适用于联合本身(而不是作为基类),所以我不明白这如何阻止派生类成为基类的完全替代品。LSP中没有规定派生类(或联合)需要成为其成员之一的替代品。 - Steve Jessop
@Steve Jessop:当然,它并不能防止使用派生类,只会使事情变得过于复杂。 - sharptooth
但再次强调,问题在哪里?你不是基本上在说“最终你会得到一个表现为联合的基类”吗?没错,但这并不出乎意料,而且也没有什么特别复杂的地方,对吧? - jalf
也许我是错的,但我不同意这个论点。如果“问题在于设置成员的数据没有存储在联合体中”,那么当允许Union作为类时,同样存在这个问题。那么为什么首先允许将Union作为类呢?如果尽管存在你提到的问题它仍被允许作为一个类,那么为什么不允许它成为基类时有相同的问题? - Alok Save
如果要求联合体有一个基础时,每个联合体成员都必须继承相同的基础,则可能会起作用。 - doug65536

7
在《C++注释参考手册》中,Bjarne Stroustrup说,“它似乎没什么理由”。

@EJP:我们大多数人都会同意“没有什么理由让联合体成为一个类”。几乎没有人将Union用作类,但它仍然被标准允许。为什么? - Alok Save
我不明白。联合体不是类。它既不能是基类,也不能有基类。我不知道你的引用出处在哪里。那不是我说的话,也不是Stroustrup说的话。Stroustrup在引文中将联合体称为基类。联合体可以像结构体一样用作类型。这是C类型系统的重要组成部分,并且支持C到最大程度是Stroustrup使用C++的目标之一。当你进入C++时,必须将“class”添加到该列表中。 - user207421
据我所知,联合体可以像结构体一样充当类的角色。 - Alok Save
请再仔细阅读我写的内容。我用心写的。类、结构体和联合体都被用作类型。你可以在任何其他合法情况下使用它们中的任何一个。这并不意味着联合体(和结构体)像类一样行事,而是它们都作为类型。 - user207421
@EJP: 明白了,所以联合体被允许作为类(类型),只是为了向后兼容到C。 - Alok Save
显然你完全没有理解重点。它们不被允许作为类来使用,这就是这个主题讨论的问题。写“classes(types)”只会延续你的困惑。而捏造引语也是如此。 - user207421

1
标题问为什么联合体不能作为基类,但问题似乎是关于联合体作为派生类的。那么哪个是正确的?
没有技术原因导致联合体不能作为基类;只是不允许这样做。一个合理的解释是将联合体视为一种结构体,其成员可能在内存中重叠,并将派生类视为继承自此(相当奇怪的)结构体的类。如果您需要该功能,通常可以说服大多数编译器接受匿名联合体作为结构体成员。以下是一个适用于作为基类使用的示例(并且联合体中还有一个匿名结构体)。
struct V3 {
    union {
        struct {
            float x,y,z;
        };
        float f[3];
    };
};

联合作为一个衍生类的理由可能更简单:结果将不会是一个联合。联合将必须是其所有成员和所有基类的并集。这很公平,并且可能会打开一些有趣的模板可能性,但您将面临一些限制(所有基类和成员都必须是 POD - 如果派生类型本质上是非 POD 的,那么您能否继承两次?),这种类型的继承与语言支持的其他类型不同(好吧,这并没有阻止 C ++),而且它有点冗余——现有的联合功能也能做得很好。
Stroustrup 在 D&E 书中说过:
与 void* 一样,程序员应该知道 unions……本质上是危险的,应该尽可能避免,在实际需要时应特别小心处理。
(省略号并未改变含义。)
所以我想这个决定是随意的,他只是看不到改变联合功能的理由(它在 C++ 的 C 子集中正常工作),因此没有设计任何与新 C++ 功能集成的东西。当风向改变时,它就被困住了。

1

我认为你在EJP的回答评论中已经得到了答案。

我认为联合体之所以被包含在C++中,仅仅是为了与C保持向后兼容。我猜想,在1970年代的内存空间极小的系统上,联合体似乎是一个好主意。但是随着C++的出现,我想联合体已经变得不那么有用了。

考虑到联合体本身就很危险,而且并不是非常有用,从联合体继承而来的创建错误的巨大新机会可能只会让人觉得这不是一个好主意 :-)


-1

你可以使用C++11的匿名联合特性继承一个联合的数据布局。

#include <cstddef>

template <size_t N,typename T>
struct VecData {
  union {
    struct {
      float x;
      float y;
      float z;
    };
    float a[N];
  };
};

template <size_t N, typename T>
  class Vec : public VecData<N,T> {
    //methods..
};

通常情况下,最好不要直接使用联合体,而是将它们封装在结构体或类中。然后,您可以以结构体外层为基础进行继承,并在其中使用联合体(如果需要)。

-1

这是我对C++ 03的猜测。

根据$9.5/1,在C++ 03中,联合体不能有虚函数。有意义的派生的整个重点在于能够覆盖派生类中的行为。如果一个联合体不能有虚函数,那么从联合体派生就没有意义。

因此有了这个规则。


1
“有意义的派生的整个目的在于能够在派生类中覆盖行为。”似乎过于宽泛。C++是一种多范式语言,而不是Chubsdad的范式语言。 - John Dibling
@John Dibling:我选择把它看作是一种幽默感 :) - Chubsdad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接