When I change a pointer in a Union, my other pointers break and show invalid pointer.
CustomDataTypeExample Class:
struct CustomDataTypeExample {
float x;
float y;
float z;
CustomDataTypeExample() = default;
CustomDataTypeExample(float x, float y, float z) {
this->x = x;
this->y = y;
this->z = z;
};
// ...
};
ConfigCustomDataTypeExample class:
struct ConfigCustomDataTypeExample {
public:
ConfigCustomDataTypeExample() = default;
ConfigCustomDataTypeExample(CustomDataTypeExample values) {
x = &values.x;
y = &values.y;
z = &values.z;
}
union {
struct {
CustomDataTypeExample* ex;
};
struct {
float* x;
float* y;
float* z;
};
};
};
main:
ConfigCustomDataTypeExample example({ 1.2f,3.4f,5.6f });
float value = 565;
example.x = &value;
std::cout << example.ex->x << ", " << example.ex->y << ", " << example.ex->z << "\n";
std::cout << *example.x << ", " << *example.y << ", " << *example.z << "\n";
Output:
565, -1.07374e+08, -1.07374e+08
565, 3.4, 5.6
What exactly is happening? If I dont change the example.x to point to something else it would work just fine otherwise if i change it then it will ruin the other pointers.
TL;DR: Three different kinds of undefined behaviour: lifetime issue, accessing a non-active member of an union (without non-standard extensions) and dereferencing an invalid pointer value through the members of
example.ex(a misunderstanding of the what the declared union represented).Looks like you could do with using plain references. The full solution is described at the end.
Deeper analysis
This is actually a really interesting problem as there is so much going on here! Three different kinds of undefined behavior. Let's go over these piece by piece.
First, like mentioned in the comments, you are assigning the address of the parameter
valuestox,yandz(addresses of the members). The parametervalueshas an automatic storage duration, which means it gets destructed at the end the constructor forConfigCustomDataTypeExample.With your program you were still able to read the values of
yandz. This is the essence of undefined behaviour: you might sometimes get sensible results, but nothing is guaranteed. For example when I tried to run your program, I got wildly different results foryandz. This was the first clear UB. Let's examine the declaration of the union next to understand what it really represents.A class is a type that consist of a sequence of members. Union is a special type of class that can hold at most one of its non-static data members at a time. The currently held object for an union is called the active member. This implies that an union is only as big as its largest data member, which is useful if memory usage is a concern.
For this union the members are the two anonymous structs (note that anonymous structs are prohibited by the C++ standard). The size of the union is determined by the largest struct, which is is the
float*struct. For a 64-bit system a the size of a pointer type is commonly 8 bytes, thus for a 64-bit system the size of this union is 24 bytes.What comes to the usage of the union, you are clearly not utilizing the union for the purpose of reducing memory consumption. Instead, you are trying to do something called type punning. Type punning is when you try interpret a binary representation of a type as another type. According to C++ standard type punning with unions is undefined behavior (second), albeit many compilers provide non-standard extensions that allow this. Let's analyze your
mainprogram according to the standard rules:Yet again, undefined behavior was kind enough to print
565when dereferencingexample.ex->x. This is because thefloat* xandexample.ex->xoverlap in the union's binary representation, albeit this is still undefined behavior.Let's first quick fix the lifetime issue by changing
ConfigCustomDataTypeExampleto take a reference as parameter:ConfigCustomDataTypeExample(CustomDataTypeExample& values)and declare aCustomDataTypeExamplevariable in main. I am also compiling with gcc, where type punning with unions is well defined (non-standard extension):Here goes nothing. The output from one of my runs is:
Ok, at least now the
x,yandzvalues are valid, but we are still getting junk values when dereferencing parts ofexample.ex. What gives? Let's go back to the declaration of our union and think how it translates to its binary representation. Here is a rough diagram:So our union's memory layout is three floating point pointers, that each point to a single floating point value (equivalent to an array that stores three floating point pointers eg.
float* arr[3]). Yet, withexample.exwe're trying to interpret thefloat* xas an array of 3 floating points. This is becauseCustomDataTypeExample's memory layout is equivalent to an array of 3 floating point values and trying to refer to its members is equivalent to array indexing.I think gcc's extension bases its interpretation of
example->exon C90 standard section 6.5.2.2 footnote 82:We can also verify this by looking at how the compiler translates these three lines to assembly:
Using godbolt we get the following (I only took the parts that are relevant):
We can see quite clearly how the compiler tries interpret the address pointed to by
example.exas region in memory that contains 3 floating point values, even though it only contains one. Hence, the first read is fine, but the second and third dereferences go very wrong.This code is produces extremely similar assembly, which is no surprise, as the behavior is equivalent:
This is case of undefined behavior is very similar to the very first case. The program is performing indirection through the invalid pointer values (third).
These three undefined behaviors combined caused the weird values to appear when you executed the
main. Now on the solution.Solution
First let's get minor nitpick out of the way.
CustomDataTypeExampleis clearly an aggregate that just encloses data inside it, so there is no need to explicitly declare special member functions for it (constructors in this case). The special member functions are implicitly declared (and trivial):What comes to the solution, it looks like you are trying to come up with an extra layer of abstraction for a simple problem. Plain references should do the trick. There is no reason for that complicated union setup, which, as you might have noticed, is quite error-prone. In C++ unions should only really be utilized for reducing memory consumption on systems, where memory is a scarce resource.
Thus, I would just get rid of the
ConfigCustomDataTypeExampleand utilize references like so:When you are working with variables that have an automatic storage duration, references are the way to go. Compared to pointers, with references lifetime issues are a little bit harder to create, and the overall solution is usually simpler.