Try to Understand Compiler Error Message: Default Member Initializer Required Before the End of Its Enclosing Class

Try to understand compiler error message: default member initializer required before the end of its enclosing class

This is a clang and gcc bug, we have a clang bug report for this: default member initializer for 'm' needed within definition of enclosing class for default argument of function which has the following example:

#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.);
}

which produces the following similar diagnostic:

t.cpp(15,34):  error: default member initializer for 'm' needed within definition of enclosing class 'A' outside of member functions
void f(double d, const B &b = B{}) {}
^
t.cpp(12,20): note: default member initializer declared here
double m = std::numeric_limits<double>::max();
^

Richard Smith indicates this is a bug:

Regarding comment#0: if we want to fix this once-and-for-all, we should use the same technique we use for delayed template parsing: teach Sema to call back into the parser to parse the delayed regions on-demand. Then we would only reject the cases where there's an actual dependency cycle.

Although does not explain why in details.

Default member initializer needed within definition of enclosing class outside of member functions - is my code ill-formed?

Your code is fine from what I can tell. Clang seems to struggle with the = default constructor rather than just defining a default constructor manually. It has the following spiel in its source code about it:

DR1351:
If the brace-or-equal-initializer of a non-static data member
invokes a defaulted default constructor of its class or of an
enclosing class in a potentially evaluated subexpression, the
program is ill-formed.

This resolution is unworkable: the exception specification of the
default constructor can be needed in an unevaluated context, in
particular, in the operand of a noexcept-expression, and we can be
unable to compute an exception specification for an enclosed class.

Any attempt to resolve the exception specification of a defaulted default
constructor before the initializer is lexically complete will ultimately
come here at which point we can diagnose it.

I think it may be incorrectly picking up the error, personally. But it specifially mentions "defaulted default constructor".

The following seems to work:

#include <utility>

struct foo
{
int x{0};
foo() noexcept {} // = default;
void f() noexcept(noexcept(std::declval<foo&>())) {}
};

int main()
{
}

Error when using in-class initialization of non-static data member and nested class constructor

Is this code really incorrect or are the compilers wrong?

Well, neither. The standard has a defect -- it says both that A is considered complete while parsing the initializer for B::i, and that B::B() (which uses the initializer for B::i) can be used within the definition of A. That's clearly cyclic. Consider this:

struct A {
struct B {
int i = (A(), 0);
};
A() noexcept(!noexcept(B()));
};

This has a contradiction: B::B() is implicitly noexcept iff A() does not throw, and A() does not throw iff B::B() is not noexcept. There are a number of other cycles and contradictions in this area.

This is tracked by core issues 1360 and 1397. Note in particular this note in core issue 1397:

Perhaps the best way of addressing this would be to make it ill-formed for a non-static data member initializer to use a defaulted constructor of its class.

That's a special case of the rule that I implemented in Clang to resolve this issue. Clang's rule is that a defaulted default constructor for a class cannot be used before the non-static data member initializers for that class are parsed. Hence Clang issues a diagnostic here:

    A(const B& _b = B())
^

... because Clang parses default arguments before it parses default initializers, and this default argument would require B's default initializers to have already been parsed (in order to implicitly define B::B()).

Why is my class non default-constructible?

This is disallowed both by the text of the standard and by several major implementations as noted in the comments, but for completely unrelated reasons.

First, the "by the book" reason: the point of instantiation of A<C> is, according to the standard, immediately before the definition of B, and the point of instantiation of std::is_default_constructible<C> is immediately before that:

For a class template specialization, [...] if the specialization is
implicitly instantiated because it is referenced from within another
template specialization, if the context from which the specialization
is referenced depends on a template parameter, and if the
specialization is not instantiated previous to the instantiation of
the enclosing template, the point of instantiation is immediately
before the point of instantiation of the enclosing template.
Otherwise, the point of instantiation for such a specialization
immediately precedes the namespace scope declaration or definition
that refers to the specialization.

Since C is clearly incomplete at that point, the behavior of instantiating std::is_default_constructible<C> is undefined. However, see core issue 287, which would change this rule.


In reality, this has to do with the NSDMI.

  • NSDMIs are weird because they get delayed parsing - or in standard parlance they are a "complete-class context".
  • Thus, that = 0 could in principle refer to things in B not yet declared, so the implementation can't really try to parse it until it has finished with B.
  • Completing a class necessitates the implicit declaration of special member functions, in particular the default constructor, as C doesn't have a constructor declared.
  • Parts of that declaration (constexpr-ness, noexcept-ness) depend on the properties of the NSDMI.
  • Thus, if the compiler can't parse the NSDMI, it can't complete the class.
  • As a result, at the point when it instantiates A<C>, it thinks that C is incomplete.

This whole area dealing with delayed-parsed regions is woefully underspecified, with accompanying implementation divergence. It may take a while before it gets cleaned up.



Related Topics



Leave a reply



Submit