Amir Kirsh
Amir Kirsh

Reputation: 13852

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?

Upvotes: 4

Views: 1748

Answers (3)

Nicol Bolas
Nicol Bolas

Reputation: 474366

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.

Upvotes: 5

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275896

The code you posted has undefined behavior.

In particular, you allocate an enablePrivateCtor then delete an A.

A better way than this is to use a key type.

class A {
  int _i;
  A(): _i(7) {}
  class construction_token_t {
    explicit construction_token_t(int) {}
    friend class A;
  };
  static auto ctor_token() { 
    return construction_token_t(0);
  }
public:
  template<class...Args>
  A( construction_token_t, Args&&...args ):A(std::forward<Args>(args)...){}

  template<typename... T>
  static std::unique_ptr<A> create(T&&... t) {
    return std::make_unique<A>(ctor_token(), std::forward<T>(t)...);
  }
  void doIt() const {
    std::cout << _i << std::endl;
  }
};

We create a token which can grant another class the right to access our private ctor. The only one who can create this token is our class.

We then pass it to make_unique.


An alternative is to use the factory lambda pattern.

template<class F>
struct factory_lambda_t {
  F f;
  template<class T>
  operator T() const { return f(); }
};

template<class F>
factory_lambda_t<std::decay_t<F>>
factory( F&& f ) { return {std::forward<F>(f)}; }

In C++14 this requires move/copy ctors to be public, but in C++17 it doesn't.

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

which I think is pretty slick.

This may result in eliding constructors completely in some cases. In others, a single move occurs.

Upvotes: 2

Useless
Useless

Reputation: 67822

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.

Upvotes: 3

Related Questions