Bug 88165 - error: default member initializer for 'A::B::m' required before the end of its enclosing class
Summary: error: default member initializer for 'A::B::m' required before the end of it...
Status: RESOLVED DUPLICATE of bug 96645
Alias: None
Product: gcc
Classification: Unclassified
Component: c++ (show other bugs)
Version: 8.2.0
: P3 normal
Target Milestone: ---
Assignee: Not yet assigned to anyone
URL:
Keywords: rejects-valid
Depends on:
Blocks: NSDMI
  Show dependency treegraph
 
Reported: 2018-11-23 11:55 UTC by leanid
Modified: 2023-12-13 21:23 UTC (History)
5 users (show)

See Also:
Host:
Target:
Build:
Known to work:
Known to fail:
Last reconfirmed: 2019-10-28 00:00:00


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description leanid 2018-11-23 11:55:47 UTC
The following compiles with MSVC2017 and not with gcc8.2. This bug was reported first on stackoverflow (https://stackoverflow.com/questions/53408962/try-to-understand-compiler-error-message-default-member-initializer-required-be)

minimum code example:
-prog.cc-------------------------
#include <limits>
class A
{
   public:
      class B
      {
         public:
            explicit B() = default;
            ~B() = default;
            
         private:
            double m = std::numeric_limits<double>::max();
      };
      
   void f(double d, const B &b = B{}) {}
};

int main()
{
   A a{};
   a.f(0.);
}
--------------------------------
compile command:
g++ prog.cc -std=c++17 
--------------------------------
error message:
prog.cc:15:36: error: default member initializer for 'A::B::m' required before the end of its enclosing class
    void f(double d, const B &b = B{}) {}
                                    ^
prog.cc:12:22: note: defined here
             double m = std::numeric_limits<double>::max();
                      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Comment 1 leanid 2018-11-23 12:03:23 UTC
minim code:
---------------
struct A
{
      struct B
      {
            double m = 0.;
      };
      
   void f(double d, const B &b = B{}) {}
};

int main()
{
}
---------------
Comment 2 Toni Neubert 2019-10-28 11:33:37 UTC
Any update on this?
Comment 3 Marek Polacek 2019-10-28 11:39:59 UTC
As of now, this code is still rejected.
Comment 4 Marek Polacek 2019-10-28 12:23:17 UTC
Maybe we need more delayed parsing here.
Comment 5 Andrew Pinski 2021-12-14 05:07:33 UTC
Hmm, clang also rejects this ...
Comment 6 Andrew Pinski 2021-12-14 05:16:44 UTC
DR 1352 might be related.
Comment 7 Fedor Chelnokov 2022-01-13 06:55:10 UTC
This struct definition:
```
struct A {
    struct B {
        int i = 0;
        B() {}
    };
    A(B = {});
};
```
is accepted by GCC, but another one ({} replaced with = default) is not:
```
struct C {
    struct D {
        int i = 0;
        D() = default;
    };
    C(D = {});
};
```
Demo: https://gcc.godbolt.org/z/WTPdTn1Yf

Could you please explain why? I though that both must be same accepted or same rejected.
Comment 8 JC Liang 2022-04-22 08:18:31 UTC
Also suffering from this issue, it's really annoying and diverged from msvc...
Comment 9 Jonathan Wakely 2022-04-22 08:26:21 UTC
This is PR 96645 and the examples on comment 1 and comment 7 compile with GCC trunk. The example in comment 0 doesn't compile still, see PR 96645 for details.

*** This bug has been marked as a duplicate of bug 96645 ***
Comment 10 Jonathan Wakely 2022-04-22 08:35:10 UTC
(In reply to Fedor Chelnokov from comment #7)
> This struct definition:
> ```
> struct A {
>     struct B {
>         int i = 0;
>         B() {}

This declares default constructor, meaning that the type is default constructible. Period.

By declaring B(); you assert that B is default constructible.

>     };
>     A(B = {});

This is valid, because the user-provided default constructor for B is all the compiler needs to see to know that B={} is valid.

> };
> ```
> is accepted by GCC, but another one ({} replaced with = default) is not:
> ```
> struct C {
>     struct D {
>         int i = 0;
>         D() = default;

This declares a default constructor that might be defined implicitly by the compiler, **or** it might get deleted if the member definitions of D would make the implicit default constructor ill-formed. This is obviously very different from the case where you declare B(); There is no assertion that D is default constructible, the compiler has to deduce whether or not that's true. That depends on the default member initializer for D::i. The initializer (which is just '0' here) is a "complete class context" which means it is not processed until the class D is complete (this allows you to use other members, or e.g. sizeof(D) as the initializer).

A nested class like C::D is not complete until its enclosing class is complete. This means the initializer for C::D::i is compiled after C is complete. This means whether C::D is default constructible is not known until C is complete.


>     };
>     C(D = {});

This requires checking whether C::D is default constructible, but we are still in the body of C, so C is not complete, which means C::D is not complete, which means we don't know if C::D is default constructible.

> };
> ```
> Demo: https://gcc.godbolt.org/z/WTPdTn1Yf
> 
> Could you please explain why? I though that both must be same accepted or
> same rejected.

I hope the explanation above helps.

GCC trunk now has a tweak to parse simple initializers like 0 immediately, instead of waiting for the class to be complete (because 0 doesn't depend on anything in the class). But for the original example in comment 0 the initializer std::numeric_limits<double>::max(); has to perform name lookup and overload resolution, and so is still delayed until the class is complete.
Comment 11 Fedor Chelnokov 2022-04-22 08:44:30 UTC
Thanks a lot for the explanation!
Comment 12 JC Liang 2022-04-22 08:55:15 UTC
(In reply to Jonathan Wakely from comment #10)
> (In reply to Fedor Chelnokov from comment #7)
> > This struct definition:
> > ```
> > struct A {
> >     struct B {
> >         int i = 0;
> >         B() {}
> 
> This declares default constructor, meaning that the type is default
> constructible. Period.
> 
> By declaring B(); you assert that B is default constructible.
> 
> >     };
> >     A(B = {});
> 
> This is valid, because the user-provided default constructor for B is all
> the compiler needs to see to know that B={} is valid.
> 
> > };
> > ```
> > is accepted by GCC, but another one ({} replaced with = default) is not:
> > ```
> > struct C {
> >     struct D {
> >         int i = 0;
> >         D() = default;
> 
> This declares a default constructor that might be defined implicitly by the
> compiler, **or** it might get deleted if the member definitions of D would
> make the implicit default constructor ill-formed. This is obviously very
> different from the case where you declare B(); There is no assertion that D
> is default constructible, the compiler has to deduce whether or not that's
> true. That depends on the default member initializer for D::i. The
> initializer (which is just '0' here) is a "complete class context" which
> means it is not processed until the class D is complete (this allows you to
> use other members, or e.g. sizeof(D) as the initializer).
> 
> A nested class like C::D is not complete until its enclosing class is
> complete. This means the initializer for C::D::i is compiled after C is
> complete. This means whether C::D is default constructible is not known
> until C is complete.
> 
> 
> >     };
> >     C(D = {});
> 
> This requires checking whether C::D is default constructible, but we are
> still in the body of C, so C is not complete, which means C::D is not
> complete, which means we don't know if C::D is default constructible.
> 
> > };
> > ```
> > Demo: https://gcc.godbolt.org/z/WTPdTn1Yf
> > 
> > Could you please explain why? I though that both must be same accepted or
> > same rejected.
> 
> I hope the explanation above helps.
> 
> GCC trunk now has a tweak to parse simple initializers like 0 immediately,
> instead of waiting for the class to be complete (because 0 doesn't depend on
> anything in the class). But for the original example in comment 0 the
> initializer std::numeric_limits<double>::max(); has to perform name lookup
> and overload resolution, and so is still delayed until the class is complete.

This is an excellent explanation, hope it benefits everyone who got here from Google search.
Comment 13 andysem 2022-04-22 10:29:22 UTC
(In reply to Jonathan Wakely from comment #10)
> (In reply to Fedor Chelnokov from comment #7)
> > This struct definition:
> > ```
> > struct A {
> >     struct B {
> >         int i = 0;
> >         B() {}
> 
> This declares default constructor, meaning that the type is default
> constructible. Period.
> 
> By declaring B(); you assert that B is default constructible.
> 
> >     };
> >     A(B = {});
> 
> This is valid, because the user-provided default constructor for B is all
> the compiler needs to see to know that B={} is valid.
> 
> > };
> > ```
> > is accepted by GCC, but another one ({} replaced with = default) is not:
> > ```
> > struct C {
> >     struct D {
> >         int i = 0;
> >         D() = default;
> 
> This declares a default constructor that might be defined implicitly by the
> compiler, **or** it might get deleted if the member definitions of D would
> make the implicit default constructor ill-formed. This is obviously very
> different from the case where you declare B(); There is no assertion that D
> is default constructible, the compiler has to deduce whether or not that's
> true. That depends on the default member initializer for D::i. The
> initializer (which is just '0' here) is a "complete class context" which
> means it is not processed until the class D is complete (this allows you to
> use other members, or e.g. sizeof(D) as the initializer).
> 
> A nested class like C::D is not complete until its enclosing class is
> complete. This means the initializer for C::D::i is compiled after C is
> complete. This means whether C::D is default constructible is not known
> until C is complete.
> 
> 
> >     };
> >     C(D = {});
> 
> This requires checking whether C::D is default constructible, but we are
> still in the body of C, so C is not complete, which means C::D is not
> complete, which means we don't know if C::D is default constructible.

Does it? I thought the default arguments were only evaluated at the point where they are actually used, i.e. in this case - when `C` constructor is invoked with no arguments.

> > };
> > ```
> > Demo: https://gcc.godbolt.org/z/WTPdTn1Yf
> > 
> > Could you please explain why? I though that both must be same accepted or
> > same rejected.
> 
> I hope the explanation above helps.

Thank you for the explanation.

If this is how the standard specifies compiler behavior, I wonder if this should be considered as a defect (DR). There is nothing in `std::numeric_limits<double>::max()` that depends on the definition of `C`, so there is no reason to prevent it from compiling.
Comment 14 Jonathan Wakely 2022-04-22 10:42:52 UTC
(In reply to andysem from comment #13)
> (In reply to Jonathan Wakely from comment #10)
> > >     C(D = {});
> > 
> > This requires checking whether C::D is default constructible, but we are
> > still in the body of C, so C is not complete, which means C::D is not
> > complete, which means we don't know if C::D is default constructible.
> 
> Does it? I thought the default arguments were only evaluated at the point
> where they are actually used, i.e. in this case - when `C` constructor is
> invoked with no arguments.

It's surprising, I agree. Whether the default argument is valid determines whether this constructor is considered a default constructor, which determines whether the compiler needs to implicitly declare a separate default constructor I think that's why the default argument is needed earlier than you expect.

> If this is how the standard specifies compiler behavior, I wonder if this
> should be considered as a defect (DR). 

I closed this as a dup of PR 96645 which has a DR number right there in the title. Like I said, see there for more details.

> There is nothing in
> `std::numeric_limits<double>::max()` that depends on the definition of `C`,
> so there is no reason to prevent it from compiling.

Are you sure? What if C::std is declared? Then the initializer means something very different. Again, see PR 96645 for further discussion of when the initializer can be compiled (and a possible patch to do it differently).