Trouble With Generic Smart Pointer Factories

Two Strikes Against std::make_unique and std::make_shared

In this article, we examine whether the two generic factory functions mentioned in the title can be used as a full replacement for explicit allocation. Spoiler alert: there are troublesome corner cases.

This post is a follow-up to the previous article: Eradication of the Raw Pointer in C++11. It closed with a quote by Herb Sutter:

Avoid using plain new or other raw unmanaged allocation directly.

The Joys of Raw Pointer-Agnostic Programming

So following Herb Sutter, we ideally never want to see a pointer because of direct allocation at all. Prior to C++11 it was common to add a static create factory method to each function. Which means a lot of repetitive typing as we need to forward all arguments to the constructor (which is private here to disable the stack for this object). And we still have an explicit new in the code here. Check it out:

#include <memory>

class ImplDetail{};

class Object : private ImplDetail
{
private:
    // ________________________________________________________________________
    Object(
        int a,
        int b,
        int c){ /* omitted members */}
public:
    typedef   std::auto_ptr<Object>APtr;
    // ________________________________________________________________________
    static APtr                    create(
        int a,
        int b,
        int c)
    {return APtr(new Object(a, b, c));}
};

int                                main(
    int  argc,
    char*argv[])
{
    // c++03 style
    Object::APtr autoObject =  Object::create(1, 2, 3);
    return 0;
}

Luckily, C++11 introduced std::make_shared which is template functions that yields a std::share_ptr to the object. Thus, we how have a neat generic factory function. C++14 added std::make_unique which is the same thing for a std::unique_ptr (if we are limited to C++11 you can emulate it). The best thing about it: these generic factory functions completely hide the new invocation. Check out the code now:

#include <memory>

class Object
{
public:
    // ________________________________________________________________________
    Object(
        int a,
        int b,
        int c){ /* omitted members */}
};


int                    main(
    int  argc,
    char*argv[])
{
    // c++11 style (needs c++14 to compile or emulate make_unique)
    auto ownedUnique = std::make_unique<Object>(1, 2, 3);
    auto ownedShared = std::make_shared<Object>(1, 2, 3);

    return 0;
}

Excellent! Now we have everything to completely erase new/new[] completely from our memories, correct?

The Woes of Raw Pointer-Agnostic Programming

And here’s the sad news of this article: No, the generic factory functions are not as universal as they seem.

Private Constructors

In our C++03 example we wanted a private constructor for reasons of lifetime clarity (Good Thing TM). std::make_unique<Object> does not see Object(), therefore you get a compile error. Now you have three options:

  • You grant friendship to a specialized std::make_unique<Object>. A little bit awkward, but friendship with a generic factory does not violate encapsulation. Okay, but what if std::make_unique<Object> delegates construction (see Q2 in N3588)to other functions as an implementation detail? And library developers really like to do that for various reasons. For your solution at hand, this means: Game over. This is not portable.

  • You refrain from using a generic factory method and use a static factory method like createUnique(). This works fine unless you find yourself using a template function that calls std::make_unique<Object>. If you write client code using this template function, this essentially means go to item 1 (i.e., game over). If you own the template function, you can awkwardly code around the problem.

  • You make the constructor public. That means the generic factory function has forced a design choice upon your code. I hate it, when generic factory functions do that…

Private Inheritance

Okay, we grind our teeth and grant public visibility to the constructor of Object. Let’s go back to our previous c++03 example. Object also inherited an implementation detail (called ImplDetail). I find myself using private inheritance quite often when passing handlers to object who receive messages asynchronously. What does private inheritance mean for std::make_unique? Here is an example:

#include <memory>

class Interface{ /* virtual functions */ };

class Implementer : private Interface
{
public:
    explicit Implementer() {}
};

class ClientObject
{
public:
    // ________________________________________________________________________
    ClientObject(
        Interface& interface)
        : d_interface(interface){}
    // ________________________________________________________________________
    virtual ~ClientObject(){}
private:
    Interface& d_interface;
};

int                    main(
    int  argc,
    char*argv[])
{
    Implementer impl;

    // does not compile
    auto        ptr = std::make_unique<ClientObject>(impl);

    return 0;
}

Depending on your compiler, you receive an error message like:

error: ‘Interface’ is an inaccessible base of ‘Implementer’

In other words since make_unique<ClientObject> does not know that Implementer implements an Interface. For this issue, we do not even have
friendship to help us out. Once more, the generic factory functions will dictate a design decision to you. The entire world will need to know that Implements is an Interface.

Conclusion

My enthusiasm for the generic factory functions std::make_unique and std::make_shared has declined. Previously, I thought that these two functions may be the key to future devoid of any raw pointers. I am curious to see whether and how the issues described above will be addressed by the committee.

1 Comment

From: lucas
2017-03-02 16:38
Hi Andre,
here is another solution for the private ctor problem, using a private tag class: http://seanmiddleditch.com/enabling-make_unique-with-private-constructors/
Let me know what you think about it Lucas

Post a Comment

All comments are held for moderation; Markdown formatting accepted;
By posting you agree to the following privacy policy.

This is a honeypot form. Do not use this form unless you want to get your IP address blacklisted. Use the second form below for comments.
Name: (required)
E-mail: (required, not published)
Website: (optional)
Name: (required)
E-mail: (required, not published)
Website: (optional)