【问题标题】:Default Initialization of a Union within a Class类中联合的默认初始化
【发布时间】:2020-11-26 09:53:32
【问题描述】:

我正在将一些旧的 c++ 代码从 Visual Studio 2013 更新到 VS2019,并遇到了一个有趣的问题:

有问题的代码使用一个名为 Attrib.h 的类来表示表格单元格中任何可能的数据单元。该数据可以是 bool、int、double、Guid/Uuid、各种类型的几何数据、文本等。因此,Attrib.h 包含许多不同结构的联合,用于表示这些不同的可能数据类型。我在下面包含了一个精简版:

class Attrib
{

public:

union
{
    double  vDouble;
    bool    vBool;

    struct sUnion
    {
#ifdef _WIN64
            BYTE Data[24];
#else
            BYTE Data[16];
#endif
    } vUnion;

    union
    {
        struct
        {
            INT32   vInt32;
            INT32   vInt32High; 
        };
        INT64       vInt64;
    };
    struct sStr
    {
        //etc
    } vStr;

    struct sBin
    {
        //etc
    } vBin;

    struct sGeomPt
    {
        //etc
    } vGeomPt;

    struct sGeomMultiPt
    {
        //etc
    } vGeomMultiPt;

    struct sGeomPoly
    {
        //etc
    } vGeomPoly;

    struct
    {
        Guid vGuid;
    };
};

bool        UsePool : 1;
bool        OwnPtr  : 1;
FieldType   Type    : 8;

//Other data members, excluded for brevity. 

#define CONSTRUCT : UsePool(true), OwnPtr(false), Type(FieldTypeNull)

Attrib() CONSTRUCT
{}
}   

现在...由于我不明白的原因,每当在 VS2013 中创建 Attrib 实例(例如 Attrib myAttrib)时,编译器都会调用 Guid 类的构造函数(Union 的最后一个成员)。 Guids构造函数如下:

Guid()
{
    Q1 = 0;
    Q2 = 0;
}

Q1 和 Q2 是无符号 int64 和 Class Guid(Microsoft 的 GUID 的变体)的唯一成员。这具有将类属性联合初始化为 0 的效果,这反过来又掩盖了许多错误。简而言之,未初始化的属性应该是 Null 类型。但在某些地方,由于初始化为零,它们被错误地解释为值为 0 的 double 或 int,或值为 false 的 bool 等。

但是在切换到 VS2019 时,永远不会调用 Guid 构造函数(其余代码未更改,因此我认为这是编译器的“决定”)。结果, Attrib 类的 Union 组件未初始化为 0,而是初始化为大量负数(在整数或双精度数的情况下),在布尔值的情况下为“真”等。这具有取消屏蔽的效果一些以前的错误,最终是一件好事。

但是,我的问题是:

  1. 编译器在 2013 年调用 Guid 构造函数,然后在 2019 年不再这样做的原因是什么?我没有更改这些类中的任何代码,所以我假设这些是对编译器规则的更改。有什么地方可以读到这个吗?

  2. 依赖此默认 2013 行为的案例是错误,我正在追踪它们。但是,与此同时,我需要复制 VS2013 构建的默认行为并将 Union 初始化为零,以便可以使用代码。我只是简单地更新了 Attrib.h 的构造函数以调用 vUnion 结构,它具有对所有内容进行零初始化的效果。即:

    #define CONSTRUCT : UsePool(true), OwnPtr(false), Type(FieldTypeNull), vUnion()

这完全符合我的需要,而且对于一个经常实例化的类来说似乎没有明显的额外成本。但是有没有更好的方法呢?

【问题讨论】:

    标签: c++ visual-studio visual-studio-2019 unions


    【解决方案1】:

    首先,由于Guid 有一个构造函数,包含这样一个Guid 的联合是一个“无限制联合”。在 C++98 中,这甚至是不允许的。此外,union 是匿名的。这是一个有问题的组合:不受限制的联合通常需要特殊的成员函数,但匿名联合不能拥有那些特殊的成员函数。

    您错误地假设调用了“Guid 构造函数”。它不是;联合未初始化。读取未初始化的值是未定义的行为,任何事情都可能发生。 “0”只是众多可能结果之一。

    结果可能会因月相而改变。一个新的编译器更有可能带来它自己的随机新结果。没有什么特别要读的;未定义行为未定义。这就是它的工作原理。如果有要阅读的内容,那将是实现定义的行为。

    执行此操作的“更好方法”称为std::variant。不再需要重新发明轮子。这些具有非平凡类型的匿名联合很难正确处理,标准库中有替代方法

    【讨论】:

    • 联合似乎也被用于类型双关语(这在 C++ 中是非法的)。
    • @MSalters 每次创建 Attrib 实例时,如果我在调试中单步执行代码,则会命中 Guid 构造函数。我原以为这是未定义的行为,但为什么 Guid 构造函数每次都会被命中?它是 100% 可重复的,并且是我不理解的部分内容。
    • @Nimo:这是未定义行为的一种可能形式。但请注意,即使是最小的变化也可能会影响它。切换到发布版本肯定可以改变这一点。优化器往往对未定义的行为很敏感;他们可能会做出“错误”的决定,因为他们使用标准 C++ 规则来确定允许哪些转换。删除不应该存在的 Guid 构造函数看起来是一个完全合理的优化。
    • @MSalters 好的,明白了。顺便说一句,发布和调试之间存在差异。在您的评论和原始回复之间,我很高兴这就是答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-19
    • 2015-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多