0

Recently, I came across code like this:

class NeedsFactoryForPublicCreation
{
private:
    struct Accessor
    {
      // Enable in order to make the compile failing (expected behavior)
      // explicit Accessor() = default;
    };
 
public:
    explicit NeedsFactoryForPublicCreation(Accessor) {}
    
    // This was intended to be the only public construction way
    static NeedsFactoryForPublicCreation create()
    {
        NeedsFactoryForPublicCreation result{Accessor()};
        // ... Do something (complex parametrization) further on
        return result;
    }
};

int main()
{
    //NeedsFactoryForPublicCreation::Accessor publicAccessor {}; // Does not compile as expected
    NeedsFactoryForPublicCreation nonDefaultConstructible {{}}; // Compiles even with Accessor being an interal
}

At first, I was a bit shocked, that this compiles. After some minutes of analysis (and occurring self-doubts...), I found out, that this is fully explainable due to the definition of access specifiers and the way the compiler decides what actual way of initialization is used (aggregate vs. public constructor look-up). In order to extend the first confusion, even this access class compiles this way:

class Accessor
{
    Accessor() = default; // private, but we are an aggregate...
    friend class NeedsFactoryForPublicCreation;
};

which is again fully explainable since Accessor is still an aggregate (another confusing topic...).

But in order to emphasize, that this question has nothing to do with aggregates in detail (thanks to Jarod42 in the comments to point out the upcoming C++20 changes here!), this compiles too:

class Accessor
{
    public:
    Accessor() {}
    virtual ~Accessor() {}
    virtual void enSureNonAggregate() const {}
    friend class NeedsFactoryForPublicCreation;
};

and does not as soon as the constructor becomes private.

My question: What's the actual background, the standard decided the effect of access specifiers this counterintuitively in doubt? With counterintuitively I especially mean the inconsistency of effective look-up (the compiler doesn't care about the internal as long as we stay unnamed, but then cares when it comes to actual constructor look-up, still fully unnamed context). Or maybe I'm mixing categories too much here.

I know, access specifiers' background is quite strictly naming based and there are also other ways to achieve the "publication" of this internal, but as far as I know, they are far more explicit. Legally implicitly creating an unnamed temporary of a private internal within an outer scope via this extremely implicit way looks horrible and might even be quite error prone, at latest when multiple arguments are the case (uniform initialized empty std-containers...).

Secundi
  • 1,150
  • 1
  • 4
  • 12
  • 1
    Has been fixed (`private:Accessor() = default;`) in C++20 IIRC. – Jarod42 Feb 15 '21 at 14:01
  • Rules have changed for [aggregate_initialization](https://en.cppreference.com/w/cpp/language/aggregate_initialization). (Probably to fix issue like that (Not easy to see all possible interactions)). – Jarod42 Feb 15 '21 at 14:14
  • @Jarod42 thanks! Maybe I should try to emphasize the non-aggregate case within my question further more to focus more on the core question of the look-up behvaior itself. – Secundi Feb 15 '21 at 14:16
  • That won't answer the question, but if `// This was intended to be the only public construction way` why do you need `explicit NeedsFactoryForPublicCreation(Accessor) {}` being public then? – t.niese Feb 15 '21 at 14:27
  • @t.niese, for the concrete scenario, I want to refer to std::make_unique and/or std::make_shared still. – Secundi Feb 15 '21 at 14:29
  • 1
    @t.niese: to use with `std::make_unique` for example – Jarod42 Feb 15 '21 at 14:30
  • `private` only forbids to "name" it. So with C++20 fixes, and providing a (default) private constructor, I think there are no issues. (or you wanted different rule for generated constructors for private/protected/public classes?). – Jarod42 Feb 15 '21 at 14:41
  • @t.niese: and [passkey idiom](https://arne-mertz.de/2016/10/passkey-idiom/) in generic case. – Jarod42 Feb 15 '21 at 14:43
  • @Jarod42 you're right, with C++20, there should not be any remaining issues here for the actual intention of the code I used (either I use structs with explicit default constructors or a class with private (default) constructor). But it's still confusing as hell in doubt that you can bypass obvious 'intuitive' scopes that easy. – Secundi Feb 15 '21 at 14:53
  • I'm aware that you try to find a way to prevent that `NeedsFactoryForPublicCreation nonDefaultConstructible {{}};` is done by accident. But FYI, generally, if you expose types (as return types or parameters), then those types can be retrieved and used, so with `explicit NeedsFactoryForPublicCreation(Accessor) {}` the type `Accessor` is exposed. And for the shown example you could retrieve `Accessor`, and instantiate it anywhere. – t.niese Feb 15 '21 at 14:53
  • @t.niese Yes I know, but exposing the accessor as a return type/object is quite explicit about the underlying intention. When only referring to it as a function argument, I do not see this degree of exposure a priori, maybe a question of personal preference. Worth to mention, that the whole issues here arise from the make_shared/make_unique core issue for me actually. Public constructors of classes, whether nested or not, tell me at first glance, that they are meant to be constructed "from the public" a priori. The accessor-scheme might be some kind of a design attack then. – Secundi Feb 15 '21 at 15:05
  • `What's the actual background`: _"Fixing"_ access modifiers - that currently only protect the access to the name - without breaking something looks like a problematic task. Having a public `explicit NeedsFactoryForPublicCreation(Accessor)` and a library function like `make_unqiue` (that is treated like any other function), and passes an instance of `Accessor` to the constructor (requiring it to make a copy and calling the copy constructor), ist not that much of a difference then `NeedsFactoryForPublicCreation nonDefaultConstructible {{}}` with respect of (required) access rights. – t.niese Feb 15 '21 at 15:22

0 Answers0