Oh boy, C++... grab a copy of the standard (
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf) and prepare yourself!
I compiled your structs with clang 3.5 and got this:
| union.cpp:45:21: error: call to implicitly-deleted default constructor of 'ControllerInput'
ControllerInput input;
^
union.cpp:20:17: note: default constructor of 'ControllerInput' is implicitly deleted because variant field 'buttons' has a non-trivial default constructor
ButtonState buttons[NUM_BUTTONS];
^
1 error generated.
|
The C++ specification is quite hard to read and I'm not an expert, but I tried my best to decipher it (hopefully my analysis is not wrong but this is a beast of a language). Let's go through this, step by step.
First thing is understanding what a non-trivial default constructor is. From section 12.1:
A default constructor for a class X is a constructor of class X that can be called without an argument. If
there is no user-declared constructor for class X, a constructor having no parameters is implicitly declared
as defaulted (8.4). An implicitly-declared default constructor is an inline public member of its class. A
defaulted default constructor for class X is defined as deleted if:
— X is a union-like class that has a variant member with a non-trivial default constructor,
— any non-static data member with no brace-or-equal-initializer is of reference type,
— any non-variant non-static data member of const-qualified type (or array thereof) with no brace-or-
equal-initializer does not have a user-provided default constructor,
— X is a union and all of its variant members are of const-qualified type (or array thereof),
— X is a non-union class and all members of any anonymous union member are of const-qualified type
(or array thereof), or
— any direct or virtual base class, or non-static data member with no brace-or-equal-initializer, has class
type M (or array thereof) and either M has no default constructor or overload resolution (13.3) as applied
to M’s default constructor results in an ambiguity or in a function that is deleted or inaccessible from
the defaulted default constructor.
A default constructor is trivial if it is neither user-provided nor deleted and if:
— its class has no virtual functions (10.3) and no virtual base classes (10.1), and
— no non-static data member of its class has a brace-or-equal-initializer, and
— all the direct base classes of its class have trivial default constructors, and
— for all the non-static data members of its class that are of class type (or array thereof), each such class
has a trivial default constructor.
Otherwise, the default constructor is non-trivial.
A default constructor is a constructor that can be called without any argument. The struct ButtonState has a non trivial constructor because we violate the 2nd point "no non-static data member of its class has a brace-or-equal-initializer" by the added initializers to that struct.
The next question is why we have an implicitly deleted function (deleted functions are uncallable). For that, we refer to section 9.5:
A union can have member functions (including constructors and destructors), but not virtual (10.3) functions.
A union shall not have base classes. A union shall not be used as a base class. If a union contains a non-static
data member of reference type the program is ill-formed. At most one non-static data member of a union may
have a brace-or-equal-initializer. [ Note: If any non-static data member of a union has a non-trivial default
constructor (12.1), copy constructor (12.8), move constructor (12.8), copy assignment operator (12.8), move
assignment operator (12.8), or destructor (12.4), the corresponding member function of the union must be
user-provided or it will be implicitly deleted (8.4.3) for the union. — end note ]
The union nested in struct ControllerInput contains ButtonState which has a non-trivial default constructor. This forces the union into having an implicitly deleted constructor.
But why does the compiler complain about ControllerInput having an implicitly-deleted constructor? Going back to section 12.1, we can see the requirements for causing an implicitly deleted constructor. The key one here is:
— X is a union-like class that has a variant member with a non-trivial default constructor
Where union-like class is defined in section 9.5:
A union-like class is a union or a class that has an anonymous union as a direct member. A union-like
class X has a set of variant members. If X is a union its variant members are the non-static data members;
otherwise, its variant members are the non-static data members of all anonymous unions that are members
of X.
ControllerInput is a union-like class because it has an anonymous union as a direct member. The variants of ControllerInput are the members of the anonymous, one of which is ButtonState buttons[NUM_BUTTONS]. Which means, we have ControllerInput which is a union-like class that has a variant member with a non-trivial default constructor.
Now, why do these things prevent us from compiling the code?
Section 12.6:
When no initializer is specified for an object of (possibly cv-qualified) class type (or array thereof), or the
initializer has the form (), the object is initialized as specified in 8.5.
Assuming you're declaring a ControllerInput like I did:
Then we default initialize according to section 8.5:
To default-initialize an object of type T means:
— if T is a (possibly cv-qualified) class type (Clause 9), the default constructor for T is called (and the
initialization is ill-formed if T has no accessible default constructor);
— if T is an array type, each element is default-initialized;
— otherwise, no initialization is performed.
If a program calls for the default initialization of an object of a const-qualified type T, T shall be a class type
with a user-provided default constructor.
Our type T in this case is ControllerInput, which is of class type. So we perform default initialization by calling the default constructor. But we just found that ControllerInput has an implicitly deleted default constructor! The compiler can't call it, so it has to report the error and bail out.
The next question, though, is why adding a completely empty constructor to ControllerInput allows the code to compile. The answer lies in section 12.6.2:
In a non-delegating constructor, if a given non-static data member or base class is not designated by a
mem-initializer-id (including the case where there is no mem-initializer-list because the constructor has no
ctor-initializer) and the entity is not a virtual base class of an abstract class (10.4), then
— if the entity is a non-static data member that has a brace-or-equal-initializer, the entity is initialized
as specified in 8.5;
— otherwise, if the entity is a variant member (9.5), no initialization is performed;
— otherwise, the entity is default-initialized (8.5).
Our constructor is non-delegating (doesn't call some other constructor to construct itself) and has an empty initializer list, which the specification refers to as ctor-initializer. As we discovered earlier, the union members are variant members, so we perform no initialization as specified by the second case.
Compiling and running the code with the added constructor, then printing out the union member values I get this:
1
2
3
4
5
6
7
8
9
10
11
12 | dalekim1@lispy:/home/dalekim1> clang++ --std=c++11 union.cpp -o union
dalekim1@lispy:/home/dalekim1> ./union
32630 1
dalekim1@lispy:/home/dalekim1> ./union
32654 1
dalekim1@lispy:/home/dalekim1> ./union
32702 1
dalekim1@lispy:/home/dalekim1> ./union
32760 1
dalekim1@lispy:/home/dalekim1> ./union
32521 1
dalekim1@lispy:/home/dalekim1>
|
As you can see, the union is indeed uninitialized.
You might be asking yourself right now, "Why is this so complicated?". Well, the answer is basically due to the initialization semantics that were introduced with C++ in its object model. Since everything is viewed as an "object" and objects should always be initialized in some way, they provide constructors/destructors. However, backwards compatibility with C throws a wrench into the whole thing, because you need to be able to both construct objects that are initialized properly but still behave with the C types where constructors don't exist.
Unions, in particular, are kind of nasty here because if unions contain objects, they might have constructors. What do you do if your union contains a bunch of different objects with different constructors? You don't know which constructor to call.
Through whatever convoluted logic, the C++ standards committee decided that in this particular case, the program is ill-defined. By adding the constructor to ControllerInput as you did, you get a "well-defined" program, but the behavior might not be what you expected. Digging into the C++ specification shows us that the union members are simply not initialized.
And some people say C++ is an awesome language. It's things like this (among many, many others) that have continued to make me dislike the language the longer I use it.
Oh, and gcc 4.8.3 doesn't agree with clang on this one!
Here's my code by the way:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 | #include <cstdint>
#include <cstdio>
typedef float real32_t;
#define NUM_BUTTONS 8
struct ButtonState {
uint32_t halfTransitionCount = 0;
bool isEndedDown = false;
};
struct ControllerInput {
bool isAnalog = false;
real32_t avgX = 0.f; //average x stick postion
real32_t avgY = 0.f; //average y stick postion
union {
ButtonState buttons[NUM_BUTTONS];
struct {
ButtonState directionUp;
ButtonState directionDown;
ButtonState directionLeft;
ButtonState directionRight;
ButtonState actionUp; //Y
ButtonState actionDown; //A
ButtonState actionLeft; //X
ButtonState actionRight; //B
};
};
ControllerInput()
{
}
};
int main(int argc, char *argv[])
{
ControllerInput input;
printf("%u %d\n", input.directionUp.halfTransitionCount, input.directionUp.isEndedDown);
return 0;
}
|