C++ Union, two active members only differing by CV-qualification - c++

If I have a union with two data members of the same type, differing only by CV-qualification:
template<typename T>
union A
{
private:
T x_priv;
public:
const T x_publ;
public:
// Accept-all constructor
template<typename... Args>
A(Args&&... args) : x_priv(args...) {}
// Destructor
~A() { x_priv.~T(); }
};
And I have a function f that declares a union A, thus making x_priv the active member and then reads x_publ from that union:
int f()
{
A<int> a {7};
return a.x_publ;
}
In every compiler I tested there were no errors compiling nor at runtime for both int types and other, more complex, types such as std::string and std::thread.
I went to see on the standard if this was legal behavior and I started on looking at the difference of T and const T:
6.7.3.1 [basic.type.qualifier]
The cv-qualified or cv-unqualified versions of a type are distinct types; however, they shall have the same representation and alignment requirements ([basic.align]).
This means that when declaring a const T it has the exact same representation in memory as a T. But then I found that the standard actually disallows this for some types, which I found weird, as I see no reason for it.
I started my search on accessing non-active members.
It is only legal to access the common initial sequence of T and const T if both are standard-layout types.
10.4.1[class.union]
At most one of the non-static data members of an object of union type can be active at any time [...] [ Note: One special guarantee is made in order to simplify the use of unions: If a standard-layout union contains several standard-layout structs that share a common initial sequence ([class.mem]), and if a non-static data member of an object of this standard-layout union type is active and is one of the standard-layout structs, it is permitted to inspect the common initial sequence of any of the standard-layout struct members; see [class.mem]. — end note ]
The initial sequence is basically the order of the non-static data members with a few exceptions, but since T and const T have the exact same members in the same layout, this means that the common initial sequence of T and const T is all of the members of T.
10.3.22 [class.mem]
The common initial sequence of two standard-layout struct ([class.prop]) types is the longest sequence of non-static data members and bit-fields in declaration order, starting with the first such entity in each of the structs, such that corresponding entities have layout-compatible types, either both entities are declared with the no_­unique_­address attribute ([dcl.attr.nouniqueaddr]) or neither is, and either both entities are bit-fields with the same width or neither is a bit-field. [ Example:
And here is where the restrictions come in, it restricts some types from being accessed, even though they have the exact same representation in memory:
10.1.3 [class.prop]
A class S is a standard-layout class if it:
(3.1) has no non-static data members of type non-standard-layout class (or array of such types) or reference,
(3.2) has no virtual functions and no virtual base classes,
(3.3) has the same access control for all non-static data members,
(3.4) has no non-standard-layout base classes,
(3.5) has at most one base class subobject of any given type,
(3.6) has all non-static data members and bit-fields in the class and its base classes first declared in the same class, and
(3.7) has no element of the set M(S) of types as a base class, where for any type X, M(X) is defined as follows.108 [ Note: M(X) is the set of the types of all non-base-class subobjects that may be at a zero offset in X. — end note ]
(3.7.1) If X is a non-union class type with no (possibly inherited) non-static data members, the set M(X) is empty.
(3.7.2) If X is a non-union class type with a non-static data member of type X_0 that is either of zero size or is the first non-static data member of X (where said member may be an anonymous union), the set M(X) consists of X_0 and the elements of M(X_0).
(3.7.3) If X is a union type, the set M(X) is the union of all M(U_i) and the set containing all U_i, where each U_i is the type of the ith non-static data member of X.
(3.7.4) If X is an array type with element type X_e , the set M(X) consists of X e and the elements of M (X_e).
(3.7.5) If X is a non-class, non-array type, the set M(X) is empty.
My questions is is there any reason for this to not be valid behavior?.
Essentially is it that:
The standard makers forgot to account for this particular case?
I haven't read some part of the standard that allows this behavior?
There's some more specific reason for this not to be valid behavior?
A reason for this to be valid syntax is, for example, having a 'readonly' variable in a class, as such:
struct B;
struct A
{
... // Everything that struct A had before
friend B;
}
struct B
{
A member;
void f() { member.x_priv = 100; }
}
int main()
{
B b;
b.f(); // Modifies the value of member.x_priv
//b.member.x_priv = 100; // Invalid, x_priv is private
int x = b.member.x_publ; // Fine, x_publ is public
}
This way you don't need a getter function, which can cause performance overhead and although most compiler would optimize that away it still increases your class, and to get the variable you'd have to write int x = b.get_x().
Nor would you need a const reference to that variable (as described in this question), which while it works great, it adds size to your class, which can be bad for sufficiently big classes or classes that need to be as small as possible.
And it is weird having to write b.member.x_priv instead of b.x_priv but this would be fixable if we could have private members in anonymous unions then we could rewrite it like this:
struct B
{
union
{
private:
int x_priv;
public:
int x_publ;
friend B;
};
void f() { x_priv = 100; }
}
int main()
{
B b;
b.f(); // Modifies the value of member.x_priv
//b.x_priv = 100; // Invalid, x_priv is private
int x = b.x_publ; // Fine, x_publ is public
}
Another use case might be to give various names to the same data member, lie for example in a Shape, the user might want to refer to the position as either shape.pos, shape.position, shape.cur_pos or shape.shape_pos.
Although this would probably create more problems than it is worth, such a use case might be favorable when for example a name should be deprecated .

Code like this:
struct A { int i; };
struct B { int j; };
union U {
struct A a;
struct B b;
};
int main() {
union U u;
u.a.i = 1;
printf("%d\n", u.b.j);
}
is valid in C. For the sake of backward compatibility, it was considered desirable to ensure that it is also valid in C++. The special rules about common initial sequences of standard-layout structs ensure this backward compatibility. Extending the rule to allow more cases to be well-defined—ones involving non-standard-layout structs—is not necessary for C compatibility, since all structs that can be defined in the common subset of C and C++ are automatically standard-layout structs in C++.
Actually, the C++ rules are a little bit more permissive than required for C compatibility. They allow some cases involving base classes too:
struct A { int i; };
struct B { int j; };
struct C : A { };
struct D : B { };
// C and D have a common initial sequence consisting of C::i and D::j
But in general, structs in C++ can be much more complicated than their C counterparts. They can, for example, have virtual functions and virtual base classes, and those can affect their layout in an implementation-defined manner. For this reason, it's not so easy to make more cases of type punning through unions well-defined in C++. You would really have to sit down with implementers and discuss what the conditions would be such that the committee should mandate that two classes have the same layout for their common initial sequence and not leave it up to the implementation. Currently, that mandate applies only to standard-layout classes.
There are various rules in the standard that are strong enough to imply that T and const T always have the exact same layout even if T is not a standard-layout class. For this reason, it would be possible to make certain forms of type punning between a T member and a const T member of a union well-defined even if T is not standard-layout. However, adding only this very special case to the language is of dubious value and I think it's unlikely that the committee would accept such a proposal unless you have a really compelling use case. Not wanting to provide a getter that returns a const reference, simply because you don't want to write the () to call the getter each time you need access, is unlikely to convince the committee.

Related

Does C++20 remove the requirement for class members to be in ascending order?

In C++17 there is normative text [class.mem]/17:
Non-static data members of a (non-union) class with the same access control (Clause 14) are allocated so
that later members have higher addresses within a class object. The order of allocation of non-static data members with different access control is unspecified.
Also there is [class.mem]/24:
If a standard-layout class object has any non-static data members, its address is the same as the address
of its first non-static data member
Here are two examples:
struct A { int x, y, z; } a;
struct F { public: int p; private: int q; public: int r; } f;
According to the above standard text, C++17 guaranteed &a.x < &a.y, &a.y < &a.z, and &f.p < &f.r (but did NOT guarantee &f.p < &f.q, since F is not standard-layout so class.mem/24 does not apply).
However, in C++20 final working draft N4860, there has been a change as per CWG 2404. The [class.mem]/17 has been turned into a Note. However, notes are non-normative in ISO standards (meaning the compiler vendor can disregard them) . And I cannot find any other text that might apply.
My question is: does C++20 still somewhere specify (normatively) the guarantees &a.y < &a.z and/or &f.p < &f.r ? Or does the compiler now have the licence to reorder class members in all cases except for the first subobject of a standard-layout class?
Assuming there are no further changes between N4860 and the published standard, I guess.
This is still guaranteed by [expr.rel]/(4.2), describing the behavior of built-in <, <=, >, and >= expressions on pointer values.
If two pointers point to different non-static data members of the same object, or to subobjects of such members, recursively, the pointer to the later declared member is required to compare greater provided the two members have the same access control ([class.access]), neither member is a subobject of zero size, and their class is not a union.

Can we access a member of a non-existing union?

In the c++ standard, in [basic.lval]/11.6 says:
If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:[...]
an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),[...]
This sentence is part of the strict-aliasing rule.
Can it allow us to access the inactive member of a non existing union? As in:
struct A{
int id :1;
int value :32;
};
struct Id{
int id :1;
};
union X{
A a;
Id id_;
};
void test(){
A a;
auto id = reinterpret_cast<X&>(a).id_; //UB or not?
}
Note: Bellow an explanation of what I do not grasp in the standard, and why the example above could be useful.
I wonder in what could [basic.lval]/11.6 be usefull.
[class.mfct.non-static]/2 does forbid us to call a member function of the "casted to" union or aggregate:
If a non-static member function of a class X is called for an object that is not of type X, or of a type derived from X, the behavior is undefined.
Considering that static data member access, or static member function can directly be performed using a qualified-name (a_class::a_static_member),
the only usefull uses case of the [basic.lval]/11.6, may be to access member of the "casted to" union. I thought about using this last standard rule to implement an "optimized variant". This variant could hold either a class A object or a class B object, the two starting with a bitfield of size 1, denoting the type:
class A{
unsigned type_id_ :1;
int value :31;
public:
A():type_id_{0}{}
void bar{};
void baz{};
};
class B{
unsigned type_id_ :1;
int value :31;
public:
B():type_id_{1}{}
int value() const;
void value(int);
void bar{};
void baz{};
};
struct type_id_t{
unsigned type_id_ :1;
};
struct AB_variant{
union {
A a;
B b;
type_id_t id;};
//[...]
static void foo(AB_variant& x){
if (x.id.type_id_==0){
reinterpret_cast<A&>(x).bar();
reinterpret_cast<A&>(x).baz();
}
else if (x.id.type_id_==1){
reinterpret_cast<B&>(x).bar();
reinterpret_cast<B&>(x).baz();
}
}
};
The call to AB_variant::foo does not invoke undefined behavior as long as its argument refers to an object of type AB_variant thanks to the rule of pointer-interconvertibility [basic.compound]/4. The access to the inactive union member type_id_ is allowed because id belongs to the common initial sequence of A, B and type_id_t [class.mem]/25:
But what happens if I try to call it with a complete object of type A?
A a{};
AB_variant::foo(reinterpret_cast<AB_variant&>(a));
The problem here is that I try to access an inactive member of a union that does not exist.
The two pertinent standard paragraphs are [class.mem]/25:
In a standard-layout union with an active member of struct type T1, it is permitted to read a non-static data member m of another union member of struct type T2 provided m is part of the common initial sequence of T1 and T2; the behavior is as if the corresponding member of T1 were nominated.
And [class.union]/1:
In a union, a non-static data member is active if its name refers to an object whose lifetime has begun and has not ended.
Q3: Does the expression "its name refers" signify that "an object" is actually an object built within a living union? Or could it refers to object a because of [basic.lval]/11.6.
[expr.ref]/4.2 defines what E1.E2 means if E2 is a non-static data member:
If E2 is a non-static data member [...], the
expression designates the named member of the object designated by the
first expression.
This defines behavior only for the case where the first expression actually designates an object. Since in your example the first expression designates no object, the behavior is undefined by omission; see [defns.undefined] ("Undefined behavior may be expected when this document omits any explicit definition of behavior...").
You are also misinterpreting what "access" means in the strict aliasing rule. It means "read or modify the value of an object" ([defns.access]). A class member access expression naming a non-static data member neither reads nor modifies the value of any object and therefore is not an "access", and therefore there's never an "access ... through" a glvalue of "an aggregate or union type" by reason of a class member access expression.
[basic.lval]/11.6 is essentially copied from C, where it actually meant something because assigning or copying a struct or union accesses the object as a whole. It's meaningless in C++ because assignment and copying of class types are performed through special member functions that either performs memberwise copying (and so "accesses" the members individually) or operates on the object representation. See core issue 2051.
There are many situations, especially involving type punning and unions, where one part of the C or C++ Standard describes the behavior of some action, another part describes an overlapping class of actions as invoking UB, and the area of overlap includes some actions which should be processed consistently by all implementations as well as others that would be impractical to support on at least some implementations. Rather than trying to fully describe all cases that should be treated as defined, the authors of the Standard expected that implementations would seek to uphold the Spirit of C described in the Rationale, including the principle "Don't prevent the programmer from doing what needs to be done". This would generally lead to quality implementations giving priority to the definition of behavior when necessary to meet their customer's needs, while giving priority to the "undefinition" of behavior when that would allow optimizations that also serve their customer's needs.
The only way to treat the C or C++ Standard as defining a useful language is to recognize a category of actions whose behavior is described by one part of the Standard and classified as UB by another, and recognizing the treatment of actions in that category as a Quality of Implementation issue outside the jurisdiction of the Standard. The authors of the Standard expected compiler writers to be sensitive to their customers' needs, and thus didn't see conflicts between behavioral definitions and undefinitions as a particular problem. They thus saw no need to define terms like "object", "lvalue", "lifetime", and "access" in ways that could be applied consistently without creating such conflicts, and the definitions they created are thus not usable for purposes of deciding whether or not particular actions should be defined when such conflicts exist.
Consequently, unless or until the Standards recognize more concepts associated with objects and ways of accessing them, the question of whether a quality implementation intended to be suitable for some purpose should be expected to support a certain action will depend upon whether its authors should be expected to recognize that the action would be useful for such purpose.

will the padding of base class be copied into the derived class?

Recently, I've been reading "inside the c++ object model". It says that the padding used in base class should also be copied into the derived class, in case you want to assign the base class to the derived class. Thus, I run a test under a 64-bit computer:
class A {
public:
int valA;
char a;
};
class B : public A {
public:
char b;
};
class C : public B {
public:
char c;
};
int main(){
std::cout << sizeof(A) << " " << sizeof(B) << " " << sizeof(C)
<< std::endl;
C c;
printf("%p\n%p\n%p\n",&c,&c.b,&c.c);
}
Here is the result:
8 12 12
0x7ffd22c5072c
0x7ffd22c50734
0x7ffd22c50735
So why is C the same size of B? Although it seems that B used the 3 byte padding in A.
So why is C the same size of B?
Because trailing padding of B was reused for C::b. The padding can be reused because B is not a POD (plain old data) class (because it is not a standard layout class).
Although it seems that B used the 3 byte padding in A.
The padding of A cannot be reused for other sub objects of B, because A is a standard layout class and is trivially copyable i.e. A is a POD class.
will the padding of base class be copied into the derived class?
I suppose that you didn't mean to ask about copying, but are rather whether the base class sub object of a derived class will have the same padding as the individual type.
The answer is, as might be deduced from the above: The padding will be same, except the trailing padding may be re-used for other sub objects, unless the base class is a POD, in which case its padding can not be reused.
In the case where the padding may be reused, whether it will be is not specified by the standard, and there are differences between compilers.
Please explain or link to a definition of "standard layout types".
Current standard draft:
[basic.types]
... Scalar types, standard-layout class types ([class.prop]), arrays of such types and cv-qualified versions of these types are collectively called standard-layout types.
[class.prop] (in older versions of the standard, these may be found under [class] directly)
A class S is a standard-layout class if it:
(3.1) has no non-static data members of type non-standard-layout class (or array of such types) or reference,
(3.2) has no virtual functions and no virtual base classes,
(3.3) has the same access control for all non-static data members,
(3.4) has no non-standard-layout base classes,
(3.5) has at most one base class subobject of any given type,
(3.6) has all non-static data members and bit-fields in the class and its base classes first declared in the same class, and
(3.7) has no element of the set M(S) of types as a base class, where for any type X, M(X) is defined as follows.107 [ Note: M(X) is the
set of the types of all non-base-class subobjects that may be at a
zero offset in X. — end note]
(3.7.1) If X is a non-union class type with no (possibly inherited) non-static data members, the set M(X) is empty.
(3.7.2) If X is a non-union class type with a non-static data member of type X0 that is either of zero size or is the first
non-static data member of X (where said member may be an anonymous
union), the set M(X) consists of X0 and the elements of M(X0).
(3.7.3) If X is a union type, the set M(X) is the union of all M(Ui) and the set containing all Ui, where each Ui is the type of the
ith non-static data member of X.
(3.7.4) If X is an array type with element type Xe, the set M(X) consists of Xe and the elements of M(Xe).
(3.7.5) If X is a non-class, non-array type, the set M(X) is empty.
Item (3.6) applies in this case. Some members of B are not first declared in B. In particular, B::A::valA, and B::A::a are declared first in A. A friendlier way to describe the rule is: The class must have either no direct members, or none of its ancestors must have members. In this case both the base and the derived class have members, so it is not standard layout.
C is the same size as B because on your platform the ABI chooses the use the padding in B to store the 1-byte member C::c. B has 3 bytes of padding at the end because the entire B object has alignment 4 (due to the int member in A).
B is not the same size as A, however, because in this case the ABI apparently does not allow storing B::b in the padding of A even though there is room. This happens when all of A members are public, as they are in your example: if you make any member private, the size of A, B and C will all be 8. I believe this may be for ABI backwards compatibility, rather than motivated by any language in the standard.
I don't know if there is language in the standard which directly allows this (but there doesn't need to be), but it certainly seems that this type of padding re-use is contemplated in the case of inheritance. For example, the documentation for std::memcpy says:
If the objects are potentially-overlapping or not TriviallyCopyable, the behavior of memcpy is not specified and may be undefined.
It goes on to define potentially-overlapping:
A subobject is potentially overlapping if it is either
a base class subobject, or
a non-static data member declared with the [[no_unique_address]] attribute.
The second condition applies only in C++20.
This seems to be written to allow padding to be shared: if this clause didn't exist, memcpy on a pointer to a B subclass of C would overwrite the value of C::c which is stored in what is usually padding for B.

Union common initial sequence with primitive

I am trying to better understand a rather surprising discovery regarding unions and the common initial sequence rule. The common initial sequence rule says (class.mem 23):
 In a standard-layout union with an active member of struct type T1, it is permitted to read a non-static data member m of another union member of struct type T2 provided m is part of the common initial sequence of T1 and T2; the behavior is as if the corresponding member of T1 were nominated.
So, given:
struct A {
int a;
double x;
};
struct B {
int b;
};
union U {
A a;
B b;
};
U u;
u.a = A{};
int i = u.b.b;
This is defined behavior and i should have the value 0 (because A and B have a CIS of their first member, an int). So far, so good. The confusing thing is that if B is replaced by simply an int:
union U {
A a;
int b;
};
...
int i = u.b;
According to the definition of common initial sequence:
The common initial sequence of two standard-layout struct types is...
So CISs can only apply between two standard-layout structs. And in turn:
A standard-layout struct is a standard-layout class defined with the class-key struct or the class-key class.
So a primitive type very definitely does not qualify; that is it cannot have a CIS with anything, so A has no CIS with an int. Therefore the standard says that the first example is defined behavior, but the second is UB. This simply does not make any sense to me at all; the compiler intuitively is at least as restricted with a primitive type as with a class. If this is intentional, is there any rhyme or reason (perhaps alignment related) as to why this makes sense? Is it possibly a defect?
ninjalj has it right: the purpose of this rule is to support tagged unions (with tags at the front). While one of the types supported in such a union could be stateless (aside from the tag), this case can be trivially addressed by making a struct containing just the tag. Thus, there is no need for extending the rule beyond structs, and by default such exceptions to undefined behavior (akin to strict aliasing in this case) should be kept to a minimum for the usual reasons of optimization and flexibility of future standardization.

C++ optimise away private variable

Does ISO C++ (11) permit a private non-static class member variable to be optimised away?
This could be detected:
class X { int x; };
assert (sizeof(X) >= sizeof(int));
but I am not aware of a clause that demands the assertion above.
To clarify: (a) Is there a clause in the C++ Standard that ensure the assertion above.
(b) Can anyone think of any other way to detect the elision of x?
[offsetof?]
(c) Is the optimisation permitted anyhow, despite (a) and (b)?
I have a feeling the optimisation could be possible if the class is local to a function but not otherwise (but I'd like to have a definitive citation).
I do not think it is forbidden, but I think it is impractical.
§9 Classes [class]
7/ A standard-layout class is a class that:
has no non-static data members of type non-standard-layout class (or array of such types) or reference,
has no virtual functions (10.3) and no virtual base classes (10.1),
has the same access control (Clause 11) for all non-static data members,
has no non-standard-layout base classes,
either has no non-static data members in the most derived class and at most one base class with non-static data members, or has no base classes with non-static data members, and
has no base classes of the same type as the first non-static data member.107
8/ A standard-layout struct is a standard-layout class defined with the class-key struct or the class-key class.
... thus class X { int x; }; is a standard-layout struct.
§9.2 Class members [class.mem]
16/ Two standard-layout struct (Clause 9) types are layout-compatible if they have the same number of non-static data members and corresponding non-static data members (in declaration order) have layout-compatible types (3.9).
... thus class X { int x; }; is layout-compatible with struct Y { int y; };.
The unfortunate thing is that layout-compatible is not formally defined in the Standard. However given the use of the word layout it seems the intent is to declare that two layout-compatible types should have the same underlying representation.
Therefore, to be able to remove the x in X one would have to prove that all structures that are layout-compatible (such as Y) are amenable to the same optimization (to keep the layout compatibility). It seems quite... improbable... in any non-trivial program.