39

该标准定义了 Unions 不能用作 Base 类,但是对此有什么具体的理由吗?据我了解,联合可以有构造函数、析构函数、成员变量以及对这些变量进行操作的方法。简而言之,Union 可以封装可以通过成员函数访问的数据类型和状态。因此,在最常见的术语中,它有资格成为一个类,如果它可以作为一个类,那么为什么限制它不能作为一个基类呢?

编辑:虽然答案试图解释原因,但我仍然不明白 Union 作为派生类比 Union 作为一个类更糟糕。因此,为了获得更具体的答案和推理,我将推动这个以获得赏金。对已经发布的答案没有冒犯,谢谢!

4

7 回答 7

15

托尼帕克给出了一个非常接近事实的答案。C++ 委员会基本上认为不值得努力使联合成为 C++ 的强大部分,类似于将数组视为我们必须从 C 继承但并不真正想要的遗留东西。

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

详细说明技术问题:由于您可以将仅 POD 组件的联合包装在结构中而不会丢失任何内容,因此允许联合作为基础没有任何优势。对于仅 POD 的联合组件,显式构造函数只需分配其中一个组件,也不存在编译器生成的复制构造函数(或赋值)使用 bitblit (memcpy) 的问题。

然而,除了保留它们以便现有的 C 代码可以被认为是有效的 C++ 之外,这样的联合并没有足够的用处。这些仅 POD 的联合在 C++ 中被破坏,因为它们未能保留它们在 C 中拥有的重要不变量:任何数据类型都可以用作组件类型。

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

它遵循必须发明一些使用 mem-initialisers 初始化联合组件的规则,例如:

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

但现在的问题是:规则是什么?通常规则是你必须初始化一个类的每个成员和基类,如果你没有显式这样做,默认构造函数用于其余部分,如果一个没有显式初始化的类型没有默认构造函数,它是一个错误【异常:拷贝构造函数,默认是成员拷贝构造函数】。

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

所以现在问题变成了:你将如何编写一个拷贝构造函数?当然,如果您按照 X-Windows 事件联合的设计方式来设计联合,那么当然很有可能做到并做得对:每个组件中都有判别标记,但是您必须使用放置运算符 new 来做到这一点,你将不得不打破我上面写的乍一看是正确的规则!

默认构造函数呢?如果您没有其中之一,则不能声明未初始化的变量。

在其他情况下,您可以在外部确定组件并使用placement new 在外部管理联合,但这不是复制构造函数。事实是,如果您有 N 个组件,则需要 N 个构造函数,而 C++ 有一个错误的想法,即构造函数使用类名,这使您的名称相当短,并迫使您使用幻像类型以允许重载选择正确的构造函数..你不能为复制构造函数这样做,因为它的签名是固定的。

好的,那么有替代品吗?可能,是的,但它们并不是那么容易梦想成真,而且更难让 100 多人相信,在充斥着其他问题的三天会议中考虑值得考虑。

遗憾的是委员会没有实施上述规则:联合是强制对齐任意数据和组件的外部管理并不是真的很难手动完成,并且当代码由合适的算法生成时微不足道且完全安全,换句话说,该规则是强制性的如果您想使用 C++ 作为编译器目标语言并仍然生成可读、可移植的代码。这种具有可构造成员的联合有很多用途,但最重要的是表示包含嵌套块的函数的堆栈帧:每个块在结构中都有本地数据,每个结构都是一个联合组件,不需要任何构造函数或者这样,编译器将只使用placement new。联合提供对齐和尺寸,以及无铸造组件访问。[而且没有其他符合要求的方法来获得正确的对齐方式!]

因此,您的问题的答案是:您问错了问题。仅 POD 的联合作为基础没有任何优势,而且它们当然不能是派生类,因为那样它们就不是 POD。为了使它们有用,需要一些时间来理解为什么应该遵循 C++ 中其他地方使用的原则:丢失位不是错误,除非您尝试使用它们。

于 2010-11-26T09:42:58.717 回答
9

Union 是一种可以用作其任何成员的类型,具体取决于已设置的成员 - 以后只能读取该成员。

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

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

于 2010-11-02T07:46:49.577 回答
7

Bjarne Stroustrup 在 The Annotated C++ Reference Manual 中说“似乎没有什么理由”。

于 2010-11-02T07:50:08.940 回答
1

我认为您在对 EJP 答案的评论中自己得到了答案。

我认为联合仅包含在 C++ 中是为了向后兼容 C。我猜联合在 1970 年似乎是一个好主意,在内存空间很小的系统上。当 C++ 出现时,我想联合已经看起来没那么有用了。

鉴于联合无论如何都非常危险,而且不是非常有用,因此从联合继承会产生大量新的错误机会似乎并不是一个好主意:-)

于 2010-11-25T01:28:11.070 回答
1

标题问为什么联合不能是基类,但问题似乎是关于联合作为派生类。那么,它是什么?

工会不能成为基类没有技术原因;这是不允许的。一个合理的解释是将联合视为一个结构,其成员碰巧在内存中可能重叠,并将派生类视为继承自这个(相当奇怪的)结构的类。如果您需要该功能,通常可以说服大多数编译器接受匿名联合作为结构的成员。这是一个适合用作基类的示例。(并且在联合中有一个匿名结构可以很好地衡量。)

struct V3 {
    union {
        struct {
            float x,y,z;
        };
        float f[3];
    };
};

联合作为派生类的基本原理可能更简单:结果不会是联合。工会必须是其所有成员及其所有基地的工会。这很公平,并且可能会打开一些有趣的模板可能性,但是您会有许多限制(所有基础和成员都必须是 POD ——并且您可以继承两次,因为派生类型本质上是非-POD?),这种类型的继承将不同于其他类型的语言运动(好吧,不是说这之前已经停止了 C++)而且无论如何它有点多余——现有的联合功能也可以。

Stroustrup 在 D&E 书中这样说:

与 一样void *,程序员应该知道联合......本质上是危险的,应该尽可能避免,并且在实际需要时应该特别小心处理。

(省略不会改变意思。)

所以我认为这个决定是武断的,他只是认为没有理由改变联合功能(它与 C++ 的 C 子集一样工作正常),因此没有设计与新 C++ 功能的任何集成。当风向改变时,它就这样卡住了。

于 2010-11-23T16:51:43.633 回答
-1

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

根据 $9.5/1,在 C++ 03 中,联合不能有虚函数。有意义的派生的全部意义在于能够覆盖派生类中的行为。如果联合不能具有虚函数,则意味着从联合派生没有意义。

因此规则。

于 2010-11-23T04:50:58.837 回答
-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..
};

一般来说,最好不要直接使用联合,而是将它们包含在结构或类中。然后,您可以将继承建立在结构外层的基础上,并在需要时在内部使用联合。

于 2015-02-13T17:15:29.590 回答