Tip of the Week #134: make_unique and private constructors

Originally posted as TotW #134 on Aug 30, 2017

by Yitzhak Mandelbaum, Google Engineer

So, you read Tip #126 and are ready to leave new behind. Everything’s going fine until you try to use absl::make_unique() or std::make_unique() (C++14) to construct an object with a private constructor, and it fails to compile. Let’s take a look at a concrete example of this problem to understand what went wrong. Then, we can discuss some solutions.

Example: Manufacturing Widgets

You’re defining a class to represent widgets. Each widget has an identifier and those identifiers are subject to certain constraints. To ensure those constraints are always met, you declare the constructor of the Widget class private and provide users with a factory function, Make(), for generating widgets with proper identifiers. (See TotW #42 for advice on why factory functions are preferable to initializer methods.)

// Bad code
class Widget {
 public:
  static std::unique_ptr<Widget> Make() {
    return absl::make_unique<Widget>(GenerateId());
  }

 private:
  Widget(int id) : id_(id) {}
  static int GenerateId();

  int id_;
}

When you try to compile, you get an error like this:

error: calling a private constructor of class 'Widget'
    { return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); }
                                 ^
note: in instantiation of function template specialization
'absl::make_unique<Widget, int>' requested here
    return absl::make_unique<Widget>(GenerateId());
                ^
note: declared private here
  Widget(int id) : id_(id) {}
  ^

While Make() has access to the private constructor, absl::make_unique() does not! Note that this issue can also arise with friends. For example, a friend of Widget would have the same problem using absl::make_unique() to construct a Widget.

Recommendations

We recommend either of these alternatives:

  • Use new and WrapUnique(), but explain your choice. For example,
    // Using `new` to access a non-public constructor.
    return absl::WrapUnique(new Widget(...));
  • Consider whether the constructor may be safely exposed publicly. If so, make it public and document when direct construction is appropriate.

In many cases, marking a constructor private is over-engineering. In those cases, the best solution is to mark your constructors public and document their proper use. However, if your constructor needs to be private (say, to ensure class invariants), then use new and WrapUnique.

Why can’t I just friend absl::make_unique()?

You might be tempted to friend absl::make_unique(), which would give it access to your private constructors. This is a bad idea, for a few reasons.

First, while a full discussion of friending practices is beyond the scope of this tip, a good rule of thumb is “no long-distance friendships”. Otherwise, you’re creating a competing declaration of the friend, one not maintained by the owner. See also the style guide’s advice.

Second, notice that you are depending on an implementation detail of absl::make_unique(), namely that it directly calls new. If it is refactored so that it indirectly calls new – for example, if it is changed to call std::make_unique() once you are ready for C++14 – then the friend declaration will become useless.

Finally, by friending absl::make_unique(), you’ve allowed anyone to create your objects that way, so why not just declare your constructors public and avoid the problem altogether?

What about std::shared_ptr?

The situation is somewhat different in the case of std:shared_ptr. There is no absl::WrapShared() and the analog – std::shared_ptr<T>(new T(...)) – involves two allocations, where std::make_shared() can be done with one. If this difference is important, then consider the passkey idiom: have the constructor take a special token that only certain code can create.

For example,

class Widget {
  class Token {
   private:
    Token() {}
    friend Widget;
  };

 public:
  static std::shared_ptr<Widget> Make() {
    return std::make_shared<Widget>(Token{}, GenerateId());
  }

  Widget(Token, int id) : id_(id) {}

 private:
  static int GenerateId();

  int id_;
};

For a full discussion of the passkey idiom, consult either of these articles:


Subscribe to the Abseil Blog