为什么匿名联合体不能包含具有非平凡构造函数/析构函数的成员?

5
我可能错了,但我找到的基本解释是,联合体无法初始化,因为它不知道调用哪个成员的构造函数。编译器不能自动生成联合体的构造函数。
为什么不允许用户定义联合体的构造函数?这将消除问题,并允许存在具有非平凡构造函数/析构函数的联合体成员。
此外,为什么联合体成员不能有任何自定义构造函数?先前的解释并不适用于自定义构造函数。
更新1: 示例:
struct SQuaternion
{
    union
    {
        S3DVector Axis;
        struct
        {
            float X;
            float Y;
            float Z;
        };
    };
    float W;
};

注意:这里的问题似乎在于联合体是匿名的。因此,如何为联合体命名构造函数呢?仅仅因为它没有名称,而且没有其他原因,所以似乎不可能这样做。如果这只是一个简单的词汇问题,那将是一个可怕的理由……

更新2: 仅通过将有问题的成员包装在一个包含匿名结构中,错误就消失了。我想这是使用匿名联合体的最接近方法。它不再是一个问题,这个事实似乎很奇怪……


1
匿名联合体中不允许使用成员函数。所以你所寻找的根本就是不可能的。 - Johan Kotlinski
这差不多是我在寻找的,或者是否有其他处理方式。 - Sion Sheevok
匿名结构体也没有官方的支持。 - underscore_d
5个回答

13

一个更大的原因是:联合体怎么知道调用哪个析构函数。语言本身不追踪联合体中处于活动状态的成员。

看起来 C++0x 将允许在联合体中使用非平凡类型,如果是这样,你将被迫实现自己的构造函数和析构函数。(从提案中还不太清楚,似乎联合体的析构函数不会调用任何成员的析构函数,正确的析构函数必须手动调用。)


1
是的,这正是我的观点。这只意味着编译器不能假设,很明显用户应该被要求澄清。然而,这个选项似乎不可用。用户甚至不能这样做并被迫完全依赖于编译器的概念是有问题的。 - Sion Sheevok

2
可能有可能定义这样的东西,但这会引发太多争议,几乎不值得去做:
  1. 要构建哪个对象?
  2. 如果你定义一个union构造函数,如何区分构造一个对象和另一个对象?
  3. 每个非POD成员都需要定义一个构造函数吗?
  4. 给定当前指向的对象不同于要赋值的对象时的语法是什么?
  5. 是否允许将一个对象解释为另一个对象的上下文?这是 unions 目前所允许的,并且是一个非常普遍的用例 (union { char c[4]; int n; } u; u.n=1234; cout << u.c[1];)。
  6. 编译器在复制 unions 时如何知道调用哪个复制构造函数?
  7. 编译器在销毁 unions 时如何知道调用哪个析构函数?

(我有没有漏掉什么?)

我猜这可能被视为难以解决的问题了。


  1. 在构造函数中显式创建的对象。
  2. 在析构函数中显式销毁的对象。
  3. 在复制构造函数中显式调用的对象。
  4. 不理解您的意思,请澄清。
  5. 为什么需要那样做?
  6. 像往常一样,还必须定义显式赋值运算符。
  7. 如果删除该容量,则会删除联合体固有设计的一部分。
- Sion Sheevok
@Sion:这些问题大多数都是修辞上的。我开头说可能是有可能的。 - Marcelo Cantos

1

我认为你是对的,C++中的联合功能不够完善。它们基本上是从C语言中直接复制过来的,这意味着它们不能作为C++的变体类型。

C++中的union没有简单的方法来表示一个正确的变体类型。考虑以下代码,如果它是合法的:

union X {
    int i;
    std::string s;
};

X x;
x.s = "Hello";
x.i = 23;

无论X有多少个构造函数或析构函数,都不能确保最后一行的赋值在存储23之前调用~string。为了让编译器这样做,联合体必须包含某种指示器来存储类型。这就是为什么所有内容都必须是POD的原因。我不知道命名和未命名联合之间差异的原因,但这适用于两者。

也许如果C++联合体的所有成员都是POD,它们可以被定义为像C联合体一样,但要包含这些额外信息,并在任何非POD成员时调用正确的析构函数。但这并不是您提出的简单更改。

您可以通过编写一个类来表示当前存储的类型,然后编写构造函数、复制赋值运算符和析构函数来实现您想要放入联合体中的功能,如果允许的话。

使用char数组进行存储,使用placement new进行构造/赋值,并在析构函数中直接调用正确的析构函数。

注意对齐问题 - 您需要确保原始存储针对放置在其中的任何类型都有足够的对齐方式。一种方法是动态分配它。另一种方法是将您的字符数组与具有最大对齐要求的任何内置类型联合在一起(如果您不知道:所有内置类型都是如此)。

与您想要的联合唯一不同的使用方法是,您将不得不提供访问器来返回代理对象(可能是对象本身的引用),该对象能够正确赋值,这意味着首先调用旧类型的析构函数。然后,您可以编写x.i() = 23而不是x.i = 23

或者,您可以使用Boost.Variant


Boost.Variant更接近于联合类型。而Boost.Any则更接近于类型检查的void* - visitor

0

这段代码在我这里看起来很正常:

typedef union uAA {
    double dVal;
    int iVal[2];

    uAA() : dVal(3.22) {}
} UAA;

main() {
    UAA rdata;

    printf("Array output: %d %d \nDouble output: %lf \n",
        rdata.iVal[0], rdata.iVal[1], rdata.dVal);
}

我想我应该说一个未命名联合体。在这种情况下,我必须得出结论,为什么你不能使用未命名联合体的原因是因为你无法为未命名联合体编写构造函数,它没有名称来编写方法名称。嗯... - Sion Sheevok
@Sion,谢谢,我从来没有听说过未命名联合体……真的是奇怪和陌生的概念……现在还不确定我看到它的利益…… - Stephane Rolland
我使用了错误的术语,我应该说匿名联合,但概念是相同的。匿名联合的成员或多或少是包围该联合的成员。 - Sion Sheevok
@Sion,这只是一个品味问题,我认为union就像reinterpret_cast<>()的广义用法...所以我倾向于尽可能避免使用variant和union,因为它们是危险的实体;-)但我会记住它的存在,谢谢。 - Stephane Rolland

0

你没听错 - 对于成员而言,非平凡构造函数意味着对象旨在封装其数据内容,而联合体则消除了这种封装。将简单类型的联合体放入类中是添加封装并确保联合体内容以明智、安全方式使用的好方法。

关于联合体具有成员:来自9.5

联合体可以有成员函数(包括构造函数和析构函数),但不能有虚函数(10.3)。联合体不得具有基类。联合体不得用作基类。具有非平凡构造函数(12.1)、非平凡复制构造函数(12.8)、非平凡析构函数(12.4)或非平凡复制赋值运算符(13.5.3、12.8)的类的对象不能是联合体的成员,也不能是这些对象的数组。


是的,但那只是规则的又一次重申,我理解原因,但为什么没有能力定义匿名/未命名联合体的构造函数和析构函数以手动调用正确的成员构造函数/析构函数?这种能力存在吗? - Sion Sheevok
@Sion:你不能给联合体命名,定义构造函数/析构函数吗?为什么要保持联合体匿名?顺便说一句 - 如果你需要使用联合体并且希望它们的使用更加清晰,你应该考虑使用boost::variant。 - Tony Delroy
因为匿名联合将其所有成员提供给封闭的成员。 - Sion Sheevok

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