Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inheriting a private constructor

Trying to allow make_unique on a class with private ctor I came into the following strange difference between two cases:


Case 1 - empty ctor - compiles

class A {
    int _i;
    A(): _i(7) {}
public:
    template<typename... T>
    static std::unique_ptr<A> create(T&&... t) {
        struct enablePrivateCtor : public A {
            using A::A;
        };
        return std::make_unique<enablePrivateCtor>(std::forward<T>(t)...);
    }
    void doIt() const {
        std::cout << _i << std::endl;
    }
};

int main() {
    auto a = A::create();
    a->doIt();
}

Output:

7

Case 2 - non-empty ctor - doesn't compile

class A {
    int _i;
    A(int i): _i(i) {} // <- change 1, ctor getting int
public:
    // no change here!
    template<typename... T>
    static std::unique_ptr<A> create(T&&... t) {
        struct enablePrivateCtor : public A {
            using A::A;
        };
        return std::make_unique<enablePrivateCtor>(std::forward<T>(t)...);
    }
    void doIt() const {
        std::cout << _i << std::endl;
    }
};

int main() {
    auto a = A::create(7); // <- change 2, sending 7
    a->doIt();
}

Compilation Error:

unique_ptr.h: error: calling a private constructor of class 'enablePrivateCtor'

Why the 1st one - with the empty ctor - is OK, while the 2nd - the non-empty ctor - is not?

like image 896
Amir Kirsh Avatar asked Dec 23 '22 14:12

Amir Kirsh


2 Answers

The default constructor is never inherited. Therefore, the first enablePrivateCtor generates a default constructor, which calls the base class default constructor.

When you inherit a constructor (as in the second case), the new constructor has the same access level as the inherited one. So since A::A(int) is private, so too will be enablePrivateCtor::enablePrivateCtor(int). So you won't be able to construct with it.

If you need to have a private constructor be able to be called indirectly (through make_unique/emplace/etc), then you need to use a private key type. Like this:

class A;

class A_key
{
  A_key() = default;
  A_key(int) {} //Prevent `A_key` from being an aggregate.

  friend class A;
};

class A {
    int _i;
public:
    A(int i, A_key): _i(i) {}

    // no change here!
    template<typename... T>
    static std::unique_ptr<A> create(T&&... t)
    {
        return std::make_unique<A>(std::forward<T>(t)..., A_key{});
    }

    void doIt() const {
        std::cout << _i << std::endl;
    }
};

...

auto ptr = A::create(7);
A a(7, A_key{}); //Does not compile, since you're not a friend.

A_key is publicly copyable, but it is not publicly default constructible. So non-private code can pass them around, but non-private code cannot create them.

like image 72
Nicol Bolas Avatar answered Jan 10 '23 04:01

Nicol Bolas


The difference is that enablePrivateCtor automatically gets a default constructor (which is allowed to call A::A).

It doesn't automatically get an integer conversion constructor: add

enablePrivateCtor(int i) : A(i) {}

and see it work.

like image 40
Useless Avatar answered Jan 10 '23 05:01

Useless