Why is the defaulted default constructor deleted for a union or union-like class?

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; [...]

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<>.