27
class B {
private:
    friend class C;
    B() = default;
};

class C : public B {};
class D : public B {};

int main() {
    C {};
    D {};
    return 0;
}

I assumed that since only class C is a friend of B, and B's constructor is private, then only class C is valid and D is not allowed to instantiate B. But that's not how it works. Where am I wrong with my reasoning, and how to achieve this kind of control over which classes are allowed to subclass a certain base?

Update: as pointed out by others in the comments, the snippet above works as I initially expected under C++14, but not C++17. Changing the instantiation to C c; D d; in main() does work as expected in C++17 mode as well.

Violet Giraffe
  • 32,368
  • 48
  • 194
  • 335
  • See this: https://stackoverflow.com/questions/32235294/what-is-the-default-access-of-constructor-in-c – Diodacus Apr 05 '19 at 12:14
  • @Diodacus: so what, declaring a private constructor as default makes it public, despite being declared in the `private:` section? – Violet Giraffe Apr 05 '19 at 12:17
  • 2
    I got the error you expect: "'D::D(void)': attempting to reference a deleted function" (msvs 2017) – vahancho Apr 05 '19 at 12:17
  • `how to achieve this kind of control over which classes are allowed to sublcass a certain base`; I understand your wish, but can you explain why you would want this? Because it means that whenever you want to make an extra class, through inheritance, you need to alter the base class and this might be considered as anti-pattern – Stefan Apr 05 '19 at 12:17
  • @Diodacus: this default Ctor is explicitly private, sounds like a different question – JVApen Apr 05 '19 at 12:17
  • @VioletGiraffe No, if no contructor is explicitly provided, compiler assumes a default one that is public. In the example above C and D classes do not have contructors declared, no default public ones are assumed by compiler. – Diodacus Apr 05 '19 at 12:20
  • Try replacing `C {}; D {};` with `C c; D d;`. Compiler seems to be optimising away the former. – TrebledJ Apr 05 '19 at 12:20
  • @TrebledJ: oh, that explains why compilation only succeeds under c++17, but fails under c++14... I guess. Still don't exactly understand what changes in C++17 account for this difference in behavior. – Violet Giraffe Apr 05 '19 at 12:21
  • @vahancho: try `/std:c++17`. It only seems to compile under c++17. – Violet Giraffe Apr 05 '19 at 12:23
  • 1
    @Stefan: I hear you, but there are exactly two classes for which it is semantically meaningful to subclass `B`, and I'm trying to express/enforce this logical constraint in C++. – Violet Giraffe Apr 05 '19 at 12:24
  • [Possibly duplicated](https://stackoverflow.com/questions/33988297/deleted-default-constructor-objects-can-still-be-created-sometimes) – felix Apr 05 '19 at 12:33
  • @felix I guess that question is related but it is not a duplicate since there are no deleted constructors here. – user7860670 Apr 05 '19 at 12:35
  • @VTT But D has a deleted implicitly-declared default constructor, since D has a direct or virtual base which has a deleted default constructor, or it is ambiguous or inaccessible from this constructor. [source](https://en.cppreference.com/w/cpp/language/default_constructor) – felix Apr 05 '19 at 12:36
  • What compiler are you using? Your sample exits with a compiler error about the default constructor of D being malformed and implicitly deleted when I copy your example. using g++ (Ubuntu 5.4.0-6ubuntu1~16.04.11) 5.4.0 20160609 – Jonathon K Apr 05 '19 at 13:02
  • @JonathonK You need to compile is C++17 mode with a C++17 conforming compiler: https://godbolt.org/z/431Vyr – NathanOliver Apr 05 '19 at 13:04
  • @JonathonK: The expected error does occur with GCC 5 and 6, but does not with 7 and 8 in C++17 mode. – Violet Giraffe Apr 05 '19 at 13:05
  • @NathanOliver 5.4 does support -std=c++17 does it not? Also from that link, am I doing something wrong or do ALL versions report errors when trying to instantiate `D d;` or `D {}`? – Jonathon K Apr 05 '19 at 13:14
  • @JonathonK My apologies. It does not work. They must not have fixed that in the older versions. – NathanOliver Apr 05 '19 at 13:17
  • @NathanOliver Ah, your link included a member variable in B so it failed to compile with any version. – Jonathon K Apr 05 '19 at 13:28
  • 1
    I have never seen "there are exactly two classes for which it is semantically meaningful to subclass B, and I'm trying to express/enforce this logical constraint in C++." actually survive prolonged contact with the future. Some future programmer, perhaps even you, will curse you for failing to see their patently obvious need for another subclass. – Eric Towers Apr 05 '19 at 17:26
  • @EricTowers: then they will be free to edit the hierarchy and loosen the restriction. I understand what you mean, but sometimes you really can slice the universum in half. There is no place for a third half. You can't use a binary discriminator to split the set into more than two subsets. – Violet Giraffe Apr 05 '19 at 18:31

2 Answers2

24

This is a new feature added to C++17. What is going on is C is now considered an aggregate. Since it is an aggregate, it doesn't need a constructor. If we look at [dcl.init.aggr]/1 we get that an aggregate is

An aggregate is an array or a class with

  • no user-provided, explicit, or inherited constructors ([class.ctor]),

  • no private or protected non-static data members (Clause [class.access]),

  • no virtual functions, and

  • no virtual, private, or protected base classes ([class.mi]).

[ Note: Aggregate initialization does not allow accessing protected and private base class' members or constructors.  — end note ]

And we check of all those bullet points. You don't have any constructors declared in C or D so there is bullet 1. You don't have any data members so the second bullet doesn't matter, and your base class is public so the third bullet is satisfied.

The change that happened between C++11/14 and C++17 that allows this is that aggregates can now have base classes. You can see the old wording here where it expressly stated that bases classes are not allowed.

We can confirm this by checking the trait std::is_aggregate_v like

int main()
{
    std::cout << std::is_aggregate_v<C>;
}

which will print 1.


Do note that since C is a friend of B you can use

C c{};
C c1;
C c2 = C();
    

As valid ways to initialize a C. Since D is not a friend of B the only one that works is D d{}; as that is aggregate initialization. All of the other forms try to default initialize and that can't be done since D has a deleted default constructor.

Community
  • 1
  • 1
NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • 1
    I guess writing defaulted constructor definition out of class like `B::B() = default;` will be considered a user-provided constructor, while defaulting it in class is considered a not-user-provided constructor? – user7860670 Apr 05 '19 at 12:44
  • 2
    @VTT That makes no difference. `B() = default` inside the class is still a user declared constructor. – NathanOliver Apr 05 '19 at 12:45
  • 1
    @NathanOliver you sure? I'm quite certain that during one of the many talks in cppcon one of the lecturers explained the difference, although it may be applied elsewhere, not in this very example. Can't find the link tho. – Fureeish Apr 05 '19 at 12:46
  • 1
    @Fureeish 100% sure. What moving the constructor out of the class does change is how the object is initialized: https://stackoverflow.com/questions/54350114/c-zero-initialization-why-is-b-in-this-program-uninitialized-but-a-is-i/54350350#54350350 – NathanOliver Apr 05 '19 at 12:49
  • My first comment was about whether it is a *user-provided* constructor. Note that with out-of-class defaulted constructor [compilation will fail](https://godbolt.org/z/Y3bjsM) – user7860670 Apr 05 '19 at 12:49
  • If you write constructor, even if it is defaulted, it is user declared. – NathanOliver Apr 05 '19 at 12:50
  • @VTT Sorry, yes, moving it outside makes it user provided. In both cases though it is user declared. – NathanOliver Apr 05 '19 at 12:51
  • @VTT But that only applies to the base class. The derived class does not have a user provided constructor so it is still an aggregate. – NathanOliver Apr 05 '19 at 12:52
  • the answer need clarification regarding the difference between *user declared* and *user provided* – sp2danny Apr 05 '19 at 12:52
  • @rustyx Because an aggregate is now allowed to inherit from a non aggregate. The standard even includes an example of it: https://timsong-cpp.github.io/cppwp/n4659/dcl.init.aggr#3 – NathanOliver Apr 05 '19 at 12:59
  • Hm, interesting, and a great answer. But this difference in behavior doesn't seem to be tied to _what_ sorts of objects `B`, `C` and `D` are, but rather, _how_ they are instantiated. It seems that aggregate initialization `D d{};` is allowed, but old-style initialization `D d;` and `D();` is not? At the same time, `D d();` does compile. Now I'm puzzled. – Violet Giraffe Apr 05 '19 at 13:08
  • @VioletGiraffe Doing `D d;` requires the use of a default constructor since you are default initializing. Since `D` doesn't default constructor it can't work. `D d{}` work because it causes aggregate initialization. – NathanOliver Apr 05 '19 at 13:12
  • What do you think of this, could you explain each case? https://godbolt.org/z/GQLtMi Particularly, I'm curious about `D();` vs. `D d();` (you already explained the other cases above - thanks!). Is this the most vexing parse that succeeds because it's not an object instance declaration at all? – Violet Giraffe Apr 05 '19 at 13:12
  • 2
    @VioletGiraffe `D d2();` is the most vexing parse so you have a function, not an object. – NathanOliver Apr 05 '19 at 13:15
  • So because both B and D have no members, an aggregate instance can be created because no constructor is actually invoked? – Jonathon K Apr 05 '19 at 13:29
  • @JonathonK Yes. It circumvents the constructor call. If `B` were to have a variables then it would need to call `B`'s constructor since it isn't an aggregate and it can't do that since it is private. – NathanOliver Apr 05 '19 at 13:36
  • It's VERY bizzare that a class can have a non-aggregate base class and still be an aggregate. – Spencer Apr 05 '19 at 18:49
  • @Spencer It's not too crazy. Arrays are aggregates even though they can consist entirely of non aggregate objects. An aggregate is just an object where you provide the initialization value for the members instead of having to have N constructors to initialize its N members. – NathanOliver Apr 05 '19 at 18:53
-5

From What is the default access of constructor in c++:

If there is no user-declared constructor for class X, a constructor having no parameters is implicitly declared as defaulted. An implicitly-declared default constructor is an inline public member of its class.

If the class definition does not explicitly declare a copy constructor, one is declared implicitly. [...] An implicitly-declared copy/move constructor is an inline public member of its class.

Constructors for classes C and D are generated internally by compiler.

BTW.: If you want to play with inheritance, please make sure you have virtual destructor defined.

TrebledJ
  • 8,713
  • 7
  • 26
  • 48
Diodacus
  • 663
  • 3
  • 8
  • 1
    I think you misunderstood the point of my confusion, although your link is still relevant. I know each `C` and `D` has a default public constructor, but `D` is not supposed to be able to instantiate the instance of its base class `B` because of the latter's constructor being private. – Violet Giraffe Apr 05 '19 at 12:19
  • does this answer the question? – sp2danny Apr 05 '19 at 12:21
  • So why `B() = default;` is threated as "no user-declared constructor"? – user7860670 Apr 05 '19 at 12:23