15
struct A{
    A(){}
};
union C{
   A a;
   int b = 0;
};
int main(){
    C c;
}

In the above code, GCC and Clang both complain that the default constructor for union C is defined as deleted.

However, the relevant rule says that:

A defaulted default constructor for class X is defined as deleted if:

  • X is a union that has a variant member with a non-trivial default constructor and no variant member of X has a default member initializer,
  • X is a non-union class that has a variant member M with a non-trivial default constructor and no variant member of the anonymous union containing M has a default member initializer,

Notice the emphasized wording. In the example, IIUC, since the variant member b has a default member initializer, the defaulted default constructor shouldn't be defined as deleted. Why do these compilers report this code as ill-formed?

If change the definition of C to

union C{
   A a{};
   int b;
};

Then all compilers can compile this code. The behavior hints that the rule actually means:

X is a union that has a variant member with a non-trivial default constructor and no default member initializer is supplied for the variant member

Is this a compiler bug or the vague wording of that rule?

Boann
  • 48,794
  • 16
  • 117
  • 146
xmh0511
  • 7,010
  • 1
  • 9
  • 36
  • My understanding is that the rule is vague. BTW, C++ lacks a formal semantics. Consider using [the Clang analyzer](http://clang-analyzer.llvm.org/) or [Frama-C++](https://frama-c.com/) – Basile Starynkevitch Dec 22 '20 at 07:01
  • @BasileStarynkevitch Actually, the relevant wording itself is clear. In simple, it means that one of these variant members has a default member initializer, then the defaulted default constructor is not deleted. However, the actual behavior of compilers does not support that. – xmh0511 Dec 22 '20 at 07:03
  • 1
    Then consider submitting a patch to [GCC](http://gcc.gnu.org/) – Basile Starynkevitch Dec 22 '20 at 07:05
  • @BasileStarynkevitch I do not have that account. Moreover, I'm not sure it is a bug. – xmh0511 Dec 22 '20 at 07:09
  • You don't need an account to submit a patch to GCC. Just send an email (in written English) to [gcc-patches@gcc.gnu.org](https://gcc.gnu.org/pipermail/gcc-patches/) and this could start an interesting discussion. You'll need some account *later* – Basile Starynkevitch Dec 22 '20 at 07:29
  • 1
    @BasileStarynkevitch Well, I have done [that](https://gcc.gnu.org/pipermail/gcc-patches/2020-December/562405.html) – xmh0511 Dec 22 '20 at 08:05

2 Answers2

6

This was changed between C++14 and C++17, via CWG 2084, which added the language allowing an NSDMI on (any) union member to restore the defaulted default constructor.

The example accompanying CWG 2084 though is subtly different to yours:

struct S {
  S();
};
union U {
  S s{};
} u;

Here the NSDMI is on the non-trivial member, whereas the wording adopted for C++17 allows an NSDMI on any member to restore the defaulted default constructor. This is because, as written in that DR,

An NSDMI is basically syntactic sugar for a mem-initializer

That is, the NSDMI on int b = 0; is basically equivalent to writing a constructor with mem-initializer and empty body:

C() : b{/*but use copy-initialization*/ 0} {}

As an aside, the rule ensuring that at most one variant member of the union has an NSDMI is somewhat hidden in a subclause of class.union.anon:

4 - [...] At most one variant member of a union may have a default member initializer.

My supposition would be that since gcc and Clang already allow the above (the NSDMI on the non-trivial union member) they didn't realize that they need to change their implementation for full C++17 support.

This was discussed on the list std-discussion in 2016, with an example very similar to yours:

struct S {
    S();
};
union U {
    S s;
    int i = 1;
} u;

The conclusion was that clang and gcc are defective in rejecting, although there was at the time a misleading note, amended as a result.

For Clang, the bug is https://bugs.llvm.org/show_bug.cgi?id=39686 which loops us back to SO at Implicitly defined constructor deleted due to variant member, N3690/N4140 vs N4659/N4727. I can't find a corresponding bug for gcc.

Note that MSVC correctly accepts, and initializes c to .b = 0, which is correct per dcl.init.aggr:

5 - [...] If the aggregate is a union and the initializer list is empty, then

  • 5.4 - if any variant member has a default member initializer, that member is initialized from its default member initializer; [...]
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • Yes, [cwg2048](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#2084) has been approved such that the example should be well-formed. I wonder why GCC10.x (include Clang) still process the example via c++14 or older. – xmh0511 Dec 22 '20 at 14:31
  • @jackX I assume it's that they saw that they already supported the example in that DR (where the NSDMI is on the non-trivial member) and didn't realize that there was still work to do for full support (NSDMI on a different member). Added above. – ecatmur Dec 23 '20 at 13:24
3

Unions are a tricky thing, since all members shares the same memory space. I agree, the wording of the rule is not clear enough, since it leaves out the obvious: Defining default values for more than one member of a union is undefined behavior, or should lead to a compiler error.

Consider the following:

union U {
    int a = 1;
    int b = 0;
};

//...
U u;                 // what's the value of u.a ? what's the value of u.b ? 
assert(u.a != u.b);  // knowing that this assert should always fail. 

This should obviously not compile.

This code does compile, because A does not have an explicit default constructor.

struct A 
{
    int x;
};

union U 
{
    A a;        // this is fine, since you did not explicitly defined a
                // default constructor for A, the compiler can skip 
                // initializing a, even though A has an implicit default
                // constructor
    int b = 0;
};

U u; // note that this means that u.b is valid, while u.a has an 
     // undefined value.  There is nothing that enforces that 
     // any value contained by a struct A has any meaning when its 
     // memory content is mapped to an int.
     // consider this cast: int val = *reinterpret_cast<int*>(&u.a) 

This code cannot compile, because A::x does have an explicit default value, this collides with the eplicit default value for U::b (pun intended).

struct A 
{
    int x = 1;
};

union U 
{
    A a;
    int b = 0;
};

//  Here the definition of U is equivalent to (on gcc and clang, but not for MSVC, for reasons only known to MS):
union U
{
    A a = A{1};
    int b = 0;
};
// which is ill-formed.

This code will not compile either on gcc, for about the same reason, but will work on MSVC (MSVC is always a bit less strict than gcc, so it's not surprising):

struct A 
{
    A() {}
    int x;
};

union U 
{
    A a;
    int b = 0;
};

//  Here the definition of U is equivalent to:
union U
{
    A a = A{};  // gcc/clang only: you defined an explicit constructor, which MUST be called.
    int b = 0;
};
// which is ill-formed.

As for where the error is reported, either at the declaration or instantiation point, this depends on the compiler, gcc and msvc report the error at the initialization point, and clang will report it when you try to instantiate the union.

Note that is is highly unadvisable to have members of a union that are not bit-compatible, or at the very least bit relatable. doing so breaks type safety, and is an open invitation for bugs into your program. Type punning is OK, but for other use cases, one should consider using std::variant<>.

Michaël Roy
  • 6,338
  • 1
  • 15
  • 19
  • `u.a != u.b` is UB, as you read inactive field. – Jarod42 Dec 22 '20 at 12:40
  • A union is not a variant. All fields are active at all times. Most use case for unions are for syntactic sweetening , i.e;.: to perform implicit, transparent, casting, similar to this: `union U { uint16_t w; uint8_t b]2]; ]; ` If you need to have an active value type, one should use std::variant, or add one's own safety mechanism. This is a known issue with union. The standard DOES NOT and CANNOT define whether a membet is 'active' or not. – Michaël Roy Dec 22 '20 at 12:49
  • By safety, I mean type-safety. – Michaël Roy Dec 22 '20 at 12:54
  • 1
    The standard can define whether a member is active: it is the last written one. but indeed the information is not stored at runtime, leading to seems to works most of the type. type punning cannot be done with `union` in C++ (but can in C, so might be easier for compiler to support for both). – Jarod42 Dec 22 '20 at 12:54
  • 1
    From [cppreference](https://en.cppreference.com/w/cpp/language/union#Explanation): *"It's undefined behavior to read from the member of the union that wasn't most recently written. Many compilers implement, as a non-standard language extension, the ability to read inactive members of a union."* – Jarod42 Dec 22 '20 at 13:00
  • C++ is designed to be somewhat compatible with C code declarations, so you can do type punning in C++ just like in C. The IP stack headers are packed full of examples. IMHO, that's the only reason one should use unions in C++17. – Michaël Roy Dec 22 '20 at 13:12
  • You might write non portable code which depends of extension. but it is not **standard**. – Jarod42 Dec 22 '20 at 13:14
  • I'll add a note. – Michaël Roy Dec 22 '20 at 13:18
  • @MichaëlRoy `A a;` is equivalent to `A a = A{}; `, No, it's not. The variant member does not perform initialization if it has no default member initializer or is not designated by a mem-initializer-id, see https://timsong-cpp.github.io/cppwp/n4659/class.init#class.base.init-9.2 – xmh0511 Dec 22 '20 at 14:09
  • It tries to do the initalization. which is equivalent. Try it to your heart's content here: https://godbolt.org/z/cKsrGf A reminder: the code does not compile. – Michaël Roy Dec 22 '20 at 14:20
  • @MichaëlRoy godbolt.org/z/cKsrGf this case does not evidence that `B a` will be initialized. Since the member of class `B` has a non-static member which has a default member initializer, so its default constructor is non-trivial, see https://timsong-cpp.github.io/cppwp/n4659/class.ctor#6.2 – xmh0511 Dec 23 '20 at 01:50
  • In any of the examples above, B::b _is_ initialized. B::a can never be default-initialized, as that will generate a compile-time error. That is what the original question was about. I'vve pointed to sandbox for you to experiment with. – Michaël Roy Dec 23 '20 at 15:07
  • I Thaught that 'is equivalent to' would be clear enough for most to understand that this was what the compiler was trying to do, while generatng code. This never meant one should write this code, which is ill-formed, AS INDICATED. – Michaël Roy Dec 23 '20 at 15:11