When is a private constructor not a private constructor?
Asked Answered
C

3

97

Let's say I have a type and I want to make its default constructor private. I write the following:

class C {
    C() = default;
};

int main() {
    C c;           // error: C::C() is private within this context (g++)
                   // error: calling a private constructor of class 'C' (clang++)
                   // error C2248: 'C::C' cannot access private member declared in class 'C' (MSVC)
    auto c2 = C(); // error: as above
}

Great.

But then, the constructor turns out to not be as private as I thought it was:

class C {
    C() = default;
};

int main() {
    C c{};         // OK on all compilers
    auto c2 = C{}; // OK on all compilers
}    

This strikes me as very surprising, unexpected, and explicitly undesired behavior. Why is this OK?

Certitude answered 3/6, 2016 at 15:27 Comment(14)
Isn't C c{}; aggregate initialization so no constructor is called?Eure
What @Eure said. You don't have a user-provided constructor, so C is an aggregate.Melone
@KerrekSB At the same time, it was quite surprising to me that the user explicitly declaring a ctor does not make that ctor user-provided.Epicureanism
@Eure Yes, but it's surprising that it's aggregate initialization given that I explicitly made a constructor private... but just not explicitly explicit enough.Certitude
@Certitude For the record, I didn't know the answer on that previous question; I looked it up in response to this one.Epicureanism
@Angew That's why we're all here :)Certitude
@Angew If it was a public =default ctor, that would seem more reasonable. But the private =default ctor seems like an important thing that shouldn't be ignored. What more, class C { C(); } inline C::C()=default; being quite different is somewhat surprising.Hooknose
@Certitude I do feel it is a defect. Even though A function is user-provided if it is user-declared and not explicitly defaulted or deleted on its first declaration. says it is not user provided and thus you have an aggregate One would expect private to override that the compiler generated default is public.Eure
@Eure private does work that way. The code works because in aggregate initialization, the constructor is never called.Keithakeithley
also #33988797Twobyfour
@πάντα ῥεῖ What're you looking for on the bounty that isn't included in the answers?Certitude
@Barry It's merely about giving @jaggedSpire an incentive about something completely unrelated stuff.Jaquenette
@πάνταῥεῖ Ok I guess. ::shrug:: Hope the incentive works.Certitude
@Certitude Sometimes it does. And he put in a well achieved answer here. I'm doing such stuff rarely though.Jaquenette
E
64

The trick is in C++14 8.4.2/5 [dcl.fct.def.default]:

... A function is user-provided if it is user-declared and not explicitly defaulted or deleted on its first declaration. ...

Which means that C's default constructor is actually not user-provided, because it was explicitly defaulted on its first declaration. As such, C has no user-provided constructors and is therefore an aggregate per 8.5.1/1 [dcl.init.aggr]:

An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no private or protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3).

Epicureanism answered 3/6, 2016 at 15:35 Comment(6)
In effect, a small standard defect: the fact that the default ctor was private is in effect ignored in this context.Hooknose
@Yakk I don't feel qualified to judge that. The wording about the ctor not being user-provided looks very deliberate, though.Epicureanism
@Yakk: Well, yes and no. If the class had any data members, you'd have a chance to make those private. Without data members, there are very few situations where this situation would seriously affect anyone.Melone
@KerrekSB It matters if you're trying to use the class a sort of "access token," controlling e.g. who can call a function based on who can create an object of the class.Epicureanism
@Yakk Even more interesting is that C{} works even if the constructor is deleted.Certitude
It should be noted that C++20 will fix this, changing the wording to "no user-declared constructors".Toxic
T
57

You're not calling the default constructor, you're using aggregate initialization on an aggregate type. Aggregate types are allowed to have a defaulted constructor, so long as it's defaulted where it's first declared:

From [dcl.init.aggr]/1:

An aggregate is an array or a class (Clause [class]) with

  • no user-provided constructors ([class.ctor]) (including those inherited ([namespace.udecl]) from a base class),
  • no private or protected non-static data members (Clause [class.access]),
  • no virtual functions ([class.virtual]), and
  • no virtual, private, or protected base classes ([class.mi]).

and from [dcl.fct.def.default]/5

Explicitly-defaulted functions and implicitly-declared functions are collectively called defaulted functions, and the implementation shall provide implicit definitions for them ([class.ctor] [class.dtor], [class.copy]), which might mean defining them as deleted. A function is user-provided if it is user-declared and not explicitly defaulted or deleted on its first declaration. A user-provided explicitly-defaulted function (i.e., explicitly defaulted after its first declaration) is defined at the point where it is explicitly defaulted; if such a function is implicitly defined as deleted, the program is ill-formed. [ Note: Declaring a function as defaulted after its first declaration can provide efficient execution and concise definition while enabling a stable binary interface to an evolving code base. — end note ]

Thus, our requirements for an aggregate are:

  • no non-public members
  • no virtual functions
  • no virtual or non-public base classes
  • no user-provided constructors inherited or otherwise, which allows only constructors which are:
    • implicitly declared, or
    • explicitly declared and defined as defaulted at the same time.

C fulfills all of these requirements.

Naturally, you may be rid of this false default construction behavior by simply providing an empty default constructor, or by defining the constructor as default after declaring it:

class C {
    C(){}
};
// --or--
class C {
    C();
};
inline C::C() = default;
Tachometer answered 3/6, 2016 at 15:36 Comment(1)
I like this answer somewhat better than that by Angew, but I think it would benefit from a summary at the start in at most two sentences.Plain
C
8

Angew's and jaggedSpire's' answers are excellent and apply to . And . And .

However, in , things change a bit and the example in the OP will no longer compile:

class C {
    C() = default;
};

C p;          // always error
auto q = C(); // always error
C r{};        // ok on C++11 thru C++17, error on C++20
auto s = C{}; // ok on C++11 thru C++17, error on C++20

As pointed out by the two answers, the reason the latter two declarations work is because C is an aggregate and this is aggregate-initialization. However, as a result of P1008 (using a motivating example not too dissimilar from the OP), the definition of aggregate changes in C++20 to, from [dcl.init.aggr]/1:

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

Emphasis mine. Now the requirement is no user-declared constructors, whereas it used to be (as both users cite in their answers and can be viewed historically for C++11, C++14, and C++17) no user-provided constructors. The default constructor for C is user-declared, but not user-provided, and hence ceases to be an aggregate in C++20.


Here is another illustrative example of aggregate changes:

class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};

B was not an aggregate in C++11 or C++14 because it has a base class. As a result, B{} just invokes the default constructor (user-declared but not user-provided), which has access to A's protected default constructor.

In C++17, as a result of P0017, aggregates were extended to allow for base classes. B is an aggregate in C++17, which means that B{} is aggregate-initialization that has to initialize all the subobjects - including the A subobject. But because A's default constructor is protected, we don't have access to it, so this initialization is ill-formed.

In C++20, because of B's user-declared constructor, it again ceases to be an aggregate, so B{} reverts to invoking the default constructor and this is again well-formed initialization.

Certitude answered 9/8, 2019 at 12:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.