Reputation: 324
I'm in the process of updating some legacy c++ code from Visual Studio 2013 to VS2019 and came across an interesting problem:
The code in question uses a class called Attrib.h to represent any possible unit of data in a table's cell. This data could be a bool, int, double, Guid/Uuid, various types of geometry data, text, etc. As such, Attrib.h includes a Union of many different structs used to represent these different possible data types. I've included an abridged version below:
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
{}
}
Now... for reasons I don't understand, whenever an instance of Attrib was made (eg Attrib myAttrib) in VS2013, the compiler would call the the constructor of the class Guid (the last member of the Union). Guids constructor is as follows:
Guid()
{
Q1 = 0;
Q2 = 0;
}
Q1 and Q2 are unsigned int64s and the sole members of Class Guid (variation of Microsoft's GUID). This had the effect of initializing the Union of Class Attrib to 0, which in turn ended up masking a number of bugs. To cut a along story short, an uninitialized attribute should be of type Null. But in a few places they were wrongly being interpreted as a double or int with a value of zero, or a bool with a value of false, etc, due to the zero initialization.
However on switching to VS2019, the Guid constructor is never called (the rest of the code is unchanged and so I assume this is the compiler's "decision"). As a result the Union component of the Attrib class is not initialized to 0, but instead to massive negative numbers (in the case of ints or doubles), "true" in the case of a bool, etc. This had the effect of unmasking a handful of the previous bugs, ultimately a good thing.
However, my questions are:
What was the reason for the compiler calling the Guid constructor in 2013, and then no longer doing it in 2019? I've not changed any of the code in these classes, so I'm assuming these are changes to the rules of the compiler. Is there anywhere I can read up on this?
The cases that relied on this default 2013 behavior are bugs and I'm in the process of tracking them down. However in the mean time I need to replicate the default behavior of the VS2013 build and zero-initialize the Union so that the code can be used. I've simply updated the constructor of Attrib.h to call the vUnion struct, which has the effect of zero-initializing everything. ie:
#define CONSTRUCT : UsePool(true), OwnPtr(false), Type(FieldTypeNull), vUnion()
This works exactly as I need it to, and seemingly at no noticeable additional cost for a class that is instantiated VERY often. However is there a better way to do it?
Upvotes: 0
Views: 402
Reputation: 179877
To start with, since Guid
has a constructor, the union containing such a Guid
is an "Unrestricted union". In C++98, this was not even allowed. Also, the union
is anonymous. This is a problematic combination: unrestricted unions typically require special member functions, but anonymous unions can't have those special member functions.
You're mistakingly assuming that the "Guid constructor" is called. It's not; the union is uninitialized. Reading an uninitialized value is Undefined Behavior, anything can happen. "0" is just one of the many possible results.
The result could have changed by the phase of the moon. A new compiler is even more likely to bring its own random new outcomes. There's nothing particular to read; Undefined Behavior is undefined. That's literally how it works. If there was something to read, it would have been implementation-defined behavior.
The "better way" to do this is called std::variant
. There's no more need to reinvent the wheel. These anonymous unions with non-trivial types are very hard to get right, and the alternative is right there in the Standard Library
Upvotes: 1