62

I have this test case:

struct A{ protected: A(){} };
struct B: A{};
struct C: A{ C(){} };
struct D: A{ D() = default; };

int main(){
    (void)B{};
    (void)C{};
    (void)D{};
}

Both gcc and clang compile it in C++11 and C++14 mode. Both fail in C++17 mode:

$ clang++ -std=c++17 main.cpp 
main.cpp:7:10: error: base class 'A' has protected default constructor
        (void)B{};
                ^
main.cpp:1:22: note: declared protected here
struct A{ protected: A(){} };
                     ^
main.cpp:9:10: error: base class 'A' has protected default constructor
        (void)D{};
                ^
main.cpp:1:22: note: declared protected here
struct A{ protected: A(){} };
                     ^
2 errors generated.

$ clang++ --version
clang version 6.0.0 (http://llvm.org/git/clang.git 96c9689f478d292390b76efcea35d87cbad3f44d) (http://llvm.org/git/llvm.git 360f53a441902d19ce27d070ad028005bc323e61)
Target: x86_64-unknown-linux-gnu
Thread model: posix

(clang compiled from master Branch 2017-12-05.)

$ g++ -std=c++17 main.cpp 
main.cpp: In function 'int main()':
main.cpp:7:10: error: 'A::A()' is protected within this context
  (void)B{};
          ^
main.cpp:1:22: note: declared protected here
 struct A{ protected: A(){} };
                      ^
main.cpp:9:10: error: 'A::A()' is protected within this context
  (void)D{};
          ^
main.cpp:1:22: note: declared protected here
 struct A{ protected: A(){} };
                      ^

$ g++ --version
g++ (GCC) 8.0.0 20171201 (experimental)
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Is this change of behavior part of C++17 or is it a bug in both compilers?

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
Benjamin Buch
  • 4,752
  • 7
  • 28
  • 51
  • 2
    Changing main to `B b; C c; D d;` makes the errors go away. I am not sure what exactly `(void)B{};` does, but maybe that difference gives a hint. – Timbo Dec 05 '17 at 14:50
  • 1
    @Timbo the void is meant to drop any values, and evaluate the expression as void. It is commonly used to silence warnings about unused variable or unused expressions – Guillaume Racicot Dec 05 '17 at 14:51
  • Changing A to `protected: A() = default;` allows it to compile. Another hint. – Eljay Dec 05 '17 at 14:53
  • 1
    I think it has to do with aggregate initialization. In C++17, you can initialize parents using aggregate initialization. So when you call `B{}`, you are calling the parent's default constructor – Guillaume Racicot Dec 05 '17 at 14:54

2 Answers2

53

The definition of aggregate changed since C++17.

Before C++17

no base classes

Since C++17

no virtual, private, or protected (since C++17) base classes

That means, for B and D, they're not aggregate type before C++17, then for B{} and D{}, value-initialization will be performed, then the defaulted default constructor will be called; which is fine, because the protected constructor of base class could be called by derived class's constructor.

Since C++17, B and D become aggregate type (because they have only public base class, and note that for class D, the explicitly defaulted default constructor is allowed for aggregate type since C++11), then for B{} and D{}, aggregate-initialization will be performed,

Each direct public base, (since C++17) array element, or non-static class member, in order of array subscript/appearance in the class definition, is copy-initialized from the corresponding clause of the initializer list.

If the number of initializer clauses is less than the number of members and bases (since C++17) or initializer list is completely empty, the remaining members and bases (since C++17) are initialized by their default initializers, if provided in the class definition, and otherwise (since C++14) by empty lists, in accordance with the usual list-initialization rules (which performs value-initialization for non-class types and non-aggregate classes with default constructors, and aggregate initialization for aggregates). If a member of a reference type is one of these remaining members, the program is ill-formed.

That means the base class subobject will be value-initialized directly, the constructor of B and D are bypassed; but the default constructor of A is protected, then the code fails. (Note that A is not aggregate type because it has a user-provided constructor.)

BTW: C (with a user-provided constructor) is not an aggregate type before and after C++17, so it's fine for both cases.

Community
  • 1
  • 1
songyuanyao
  • 169,198
  • 16
  • 310
  • 405
23

In C++17, rules about aggregates has changed.

For example, you can do this in C++17 now:

struct A { int a; };
struct B { B(int){} };

struct C : A {};
struct D : B {};

int main() {
    (void) C{2};
    (void) D{1};
}

Note that we're not inheriting constructor. In C++17, C and D are now aggregates even if they have base classes.

With {}, aggregate initialization kicks in, and sending no parameters will be interpreted the same as calling the parent's default constructor from the outside.

For example, aggregate initialization can be disabled by changing the class D to this:

struct B { protected: B(){} };

struct D : B {
    int b;
private:
    int c;
};

int main() {
    (void) D{}; // works!
}

This is because aggregate initialization don't apply when having members with different access specifiers.

The reason why with = default works is because it's not a user provided constructor. More information at this question.

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141