92

The following code is quite trivial and I expected that it should compile fine.

struct A
{
    struct B
    {
        int i = 0;
    };

    B b;

    A(const B& _b = B())
        : b(_b)
    {}
};

I've tested this code with g++ version 4.7.2, 4.8.1, clang++ 3.2 and 3.3. Apart from fact that g++ 4.7.2 segfaults on this code (http://gcc.gnu.org/bugzilla/show_bug.cgi?id=57770), the other tested compilers give error messages that don't explain much.

g++ 4.8.1:

test.cpp: In constructor ‘constexpr A::B::B()’:
test.cpp:3:12: error: constructor required before non-static data member for ‘A::B::i’ has been parsed
     struct B
            ^
test.cpp: At global scope:
test.cpp:11:23: note: synthesized method ‘constexpr A::B::B()’ first required here 
     A(const B& _b = B())
                       ^

clang++ 3.2 and 3.3:

test.cpp:11:21: error: defaulted default constructor of 'B' cannot be used by non-static data member initializer which appears before end of class definition
    A(const B& _b = B())
                    ^

Making this code compilable is possible and seems like it should make no difference. There are two options:

struct B
{
    int i = 0;
    B(){} // using B()=default; works only for clang++
};

or

struct B
{
    int i;
    B() : i(0) {} // classic c++98 initialization
};

Is this code really incorrect or are the compilers wrong?

9
  • 3
    My G++ 4.7.3 says internal compiler error: Segmentation fault to this code... Commented Jul 2, 2013 at 16:08
  • 2
    (error C2864: 'A::B::i' : only static const integral data members can be initialized within a class) is what VC2010 says. That output agrees with g++. Clang says it too, though it makes much less sense. You can't default a variable in a struct by doing int i = 0 unless it is static const int i = 0. Commented Jul 2, 2013 at 16:09
  • @Borgleader: BTW I'd avoid the temptation to think of the expression B() as a function call to a constructor. You never directly "call" a constructor. Think of this as special syntax that creates a temporary B... and the constructor is invoked as just one part of that process, deep within the mechanism that follows. Commented Jul 2, 2013 at 16:15
  • 2
    Hmm, adding a constructor to B seems to make this work in gcc 4.7. Commented Jul 2, 2013 at 16:28
  • 7
    Interestingly, moving the definition of A's constructor out of A also seems to make it work (g++ 4.7); which chimes with "defaulted default constructor cannot be used... before end of class definition". Commented Jul 2, 2013 at 16:29

2 Answers 2

89
+500

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

Sign up to request clarification or add additional context in comments.

7 Comments

Good to know. But the error message is still misleading, since the constructor is not in fact "used by non-static data member initializer".
Did you know this because of a particular past experience with this issue, or just by carefully reading the standard (and list of defects)? Also, +1.
+1 for this detailed answer. So what would be the way out? Some sort of "2-phase class parsing", where the parsing of outer class members that depend on inner classes is delayed until the inner classes have been fully formed?
@aschepler Yes, the diagnostic here is not very good. I've filed llvm.org/PR16550 for that.
@Cornstalks I discovered this issue while implementing initializers for non-static data members in Clang.
|
0

Maybe this is the problem:

§12.1 5. A default constructor that is defaulted and not defined as deleted is implicitly defined when it is odr- used (3.2) to create an object of its class type (1.8) or when it is explicitly defaulted after its first declaration

So, the default constructor is generated when first looked up, but the lookup will fail because A is not completely defined and B inside A will therefore not be found.

4 Comments

I'm not sure about that "therefore". Clearly B b isn't a problem, and finding explicit methods/ an explicitly declared constructor in B is not a problem. So it would be nice to see some definition of why the lookup should proceed differently here so that "B inside A is not found" in just this one case but not the others, before we can declare the code illegal for this reason.
I found words in the standard that the class definition is considered complete during in-class initialization, including within nested classes. I didn't bother to record the reference as it didn't seem relevant.
@moonshadow: the statement says implicitly defaulted constructors are defined when odr- used. explicitly is defined after first declaration. And B b does not call an constructor, the constructor of A calls constructor of B
If anything like this were the problem, the code would still be invalid if you remove the =0 from i = 0;. But without that =0, the code is valid and you won't find a single compiler that complains about using B() within the definition of A.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.