Changed rules for protected constructors in C++17? - c++

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?

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.

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.

Related

Aggregate class and non-viable constructor template

As we know, in C++20 a class that has a user-declared constructor is not aggregate.
Now, consider the following code:
#include <type_traits>
template <bool EnableCtor>
struct test {
template <bool Enable = EnableCtor, class = std::enable_if_t<Enable>>
test() {}
int a;
};
int main() {
test<false> t {.a = 0};
}
Neither GCC nor CLang compile this code. So, the class is not an aggregate, despite the fact that there is no instantiated constructor here.
Is the constructor template considered to be a "declared constructor"? What does the Standard say about such a situation?
The definition of aggregate has changed a lot, but the relevant part here has been pretty constant. The C++20 wording in [dcl.init.aggr] says:
An aggregate is an array or a class ([class]) with
no user-declared or inherited constructors ([class.ctor]),
no private or protected direct non-static data members ([class.access]),
no virtual functions ([class.virtual]), and
no virtual, private, or protected base classes ([class.mi]).
Note that it just says no declared constructors, period. Not something subtle about whether or not the constructor is viable for a particular specialization of a class template. Just none at all.
So given something like this:
template <bool B>
struct X {
int i;
X(int i) requires B;
};
X<false> still isn't an aggregate, even though its only declared constructor isn't viable for that specialization. Doesn't matter. Aggregate-ness is a property of declarations only.

Inheriting constructors in C++20 (Visual Studio 2019)

I am using Visual Studio 2019 (v16.10.3) with /std:c++latest and this compiles:
class Base
{
public:
Base(int x) {}
};
class Derived : public Base
{
// no constructors declared in Derived
};
int main() {
Derived d(5);
}
For the previous versions of the standard I have to declare inherited constructors with the using directive:
class Derived : public Base
{
using Base::Base;
};
Is this something new that was put in C++20 or is it some Microsoft specific thing?
Is this something new that was put in C++20 or is it some Microsoft specific thing?
Not with relation to inherited constructors. What changed is that aggregate initialization may use parenthesis under certain conditions. Derived is considered an aggregate on account of having no private parts, so we initialize its bases and members directly.
It even works when we add a public member:
class Base
{
public:
Base(int ) {}
};
struct Derived : public Base
{
// no constructors declared in Derived
int y;
};
int main() {
Derived d(5, 4);
}
Live
This is the result of C++17 allowing classes with base classes to still be considered aggregates combined with C++20 rules allowing aggregate initialization to work with () constructor syntax if the values in the parentheses would work (and wouldn't call a constructor). So while it looks like a constructor call, it's actually aggregate initialization.
Derived doesn't have a constructor matching Base's constructor; you're merely initializing the aggregate Derived by providing a valid initializer for its subobject Base.

Can you use explicit constructors with designated initializers?

In the code below, is the initialization of member b legal?
class B {
public:
explicit B(int) {}
};
struct A {
B b;
};
class C {
public:
C() : a{.b{33}} {}
A a;
};
Compiling with the latest version of gcc gives this error (wandbox)
prog.cc: In constructor 'C::C()':
prog.cc:12:11: error: converting to 'B' from initializer list would use explicit constructor 'B::B(int)'
12 | C() : a{.b{33}} {}
| ^~~~~~~~~
But the latest version of clang compiles the code fine (wandbox)
Which compiler is correct?
This is a gcc bug (submitted 99566).
The rule, from [dcl.init.aggr]/4.2, is:
Otherwise, the element is copy-initialized from the corresponding initializer-clause or is initialized with the brace-or-equal-initializer of the corresponding designated-initializer-clause.
The element (b) should be initialized from the brace-or-equal-initializer ({33}). That's totally fine, that's not copy-initialization. gcc accepts B b{33}, the same kind of thing should happen here.

Strange default empty constructor on a virtual inheritance behaviour on GCC

I have the following situation of a Derived class with virtual inheritance to a Base class in my code:
class Base {
int x;
public:
Base(int x): x{x} {}
virtual void f() = 0;
};
class Derived : public virtual Base {
public:
Derived() = default;
};
class Concrete: public Derived {
public:
Concrete(): Base{42} {}
void f() override {}
};
Link: https://godbolt.org/z/bn1EY6
GCC (trunk) gives the following error: error: use of deleted function 'Derived::Derived()' while Clang (trunk) compiles it without a problem.
GCC works if I change the constructor to Derived() {} instead of Derived() = default or define an empty constructor on the Base class.
Why is the = default removing the function in GCC in this case?
Standard says (latest draft):
[class.default.ctor]
A defaulted default constructor for class X is defined as deleted if:
X is a union that ... [[does not apply]]
X is a non-union class that has a variant member M with ... [[does not apply]]
any non-static data member with no default member initializer ([class.mem]) is of reference type, [[does not apply]]
any non-variant non-static data member of const-qualified type ... [[does not apply]]
X is a union and ... [[does not apply]]
X is a non-union class and all members of any anonymous union member ... [[does not apply]]
[applies if the base is a potentially constructed subobject] any potentially constructed subobject, except for a non-static data member with a brace-or-equal-initializer, has class type M (or
array thereof) and either M has no default constructor or overload
resolution ([over.match]) as applied to find M's corresponding
constructor results in an ambiguity or in a function that is deleted
or inaccessible from the defaulted default constructor, or
any potentially constructed subobject has a type with a destructor that is deleted or inaccessible from the defaulted default constructor. [[does not apply]]
Only one rule potentially applies for the defaulted default constructor being deleted, and it depends on whether the base is a potentially constructed subobject.
[special]
For a class, its non-static data members, its non-virtual direct base classes, and, if the class is not abstract ([class.abstract]), its virtual base classes are called its potentially constructed subobjects.
Derived is abstract (because it doesn't implement all pure virtual functions), and Base is a virtual base, therefore the base is not a potentially constructed subobject, and therefore the only rule that would otherwise have applied for the defaulted constructor being deleted does not apply and thus it should not be deleted. The compiler is wrong.
A simple workaround (besides those that you already mentioned) is to no declare Derived::Derieved() at all. It seems to be correctly implicitly generated in that case.
Adding the noexcept yields the error internal compiler error
This is also a compiler bug.
Why is the = default removing the function in GCC in this case?
Whether or not this is a bug in GCC (MSVC behaves similarly but clang-cl accepts the code, as is) is a matter for those more studied in the C++ Standards. However, it appears that the complier is taking the = default to imply that the Derived constructor depends on (or is equivalent to) the default constructor for Base - which is definitely deleted, as you have defined another (non-default) constructor.
However, explicitly adding your own default constructor, with Derived() {} removes that implied dependency.
This is confirmed (in GCC and MSVC) by specifying (i.e. undeleting) the default constructor for the Base class:
class Base {
int x;
public:
Base() : x{0} {} // Adding this removes the error!
// Base() = default; // Also works
Base(int x): x{x} {}
virtual void f() = 0;
};
class Derived : public virtual Base {
public:
Derived() = default;
};
class Concrete: public Derived {
public:
Concrete(): Base{42} {}
void f() override {}
};
EDIT: This may also be relevant, or even a possible duplicate: Why is Default constructor called in virtual inheritance?

Virtual inheritance and uniform initialization in C++

Following up on this question about multiple (virtual) inheritance, I'd like to inquire about a simple MWE that makes g++ 5.2.0 upset whereas clang++ 3.6.2 handles it just fine, with no complaints at all, even with -Wall and -Wextra set. So here's the MWE:
class Z {};
class A : virtual Z { protected: A() {} };
class B : virtual Z { protected: B() {} };
class C : A, B { public: C() : A{}, B{} {} };
int main() { C c{}; return 0; }
Unlike clang++, g++ complains like this:
gccodd.c++: In constructor ‘C::C()’:
gccodd.c++:2:34: error: ‘A::A()’ is protected
class A : virtual Z { protected: A() {} };
^
gccodd.c++:4:39: error: within this context
class C : A, B { public: C() : A{}, B{} {} };
^
gccodd.c++:3:34: error: ‘B::B()’ is protected
class B : virtual Z { protected: B() {} };
^
gccodd.c++:4:39: error: within this context
class C : A, B { public: C() : A{}, B{} {} };
^
Replacing the uniform initialization in C's constructor with the old form works fine though and both clang++ and g++ are happy with the following:
class C : A, B { public: C() : A(), B() {} };
This yields the two obvious options:
The code violates the standard in some way, making the outcome undefined (i.e., any outcome would be acceptable).
One of the two compilers has a bug related to uniform initialization and multiple + virtual inheritance.
If it were a matter of voting, (1) might win, because icpc 15.0.0 says the following:
gccodd.c++(4): error #453: protected function "A::A()" (declared at line 2) is not accessible through a "A" pointer or object
class C : public virtual A, public virtual B { public: C() : A{}, B{} {} };
^
gccodd.c++(4): error #453: protected function "B::B()" (declared at line 3) is not accessible through a "B" pointer or object
class C : public virtual A, public virtual B { public: C() : A{}, B{} {} };
^
So, is it (1) or (2)? And if it's the former case, then what's wrong with my MWE?
List-initialization of an object or reference of type T is defined as
follows: (3.1) — If T is a class type and the initializer list has a
single element of type cv U [..] (3.2) — Otherwise, if T is a
character array [..] (3.3) — Otherwise, if T is an aggregate,
aggregate initialization is performed (8.5.1). (3.4) — Otherwise, if
the initializer list has no elements and T is a class type with a
default constructor, the object is value-initialized.
A and B both have base classes and hence aren't aggregates. So the fourth bullet point applies. And thus we have the exact same effect as if we had used ():
An object whose initializer is an empty set of parentheses, i.e.,
(), shall be value-initialized.
Any compiler yielding different results with those initializers cannot be conforming.
§11.4, which handles access to protected members, does not mention anything related to the form of initialization. However, concerning initialization of bases in a mem-initializer in the constructor, §11.4 is defective at the moment as mentioned by CWG issue #1883.