Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++11 make_shared instancing

Apologies for the long question, but some context is necessary. I have a bit of code that seems to be a useful pattern for the project I'm working on:

class Foo
{
public:
    Foo( int bar = 1 );
    ~Foo();
    typedef std::shared_ptr< Foo > pointer_type;
    static pointer_type make( int bar = 1 )
    {
        return std::make_shared< Foo >( bar );
    }

...
}

As you can see, it provides a straightforward way of constructing any class as a PointerType which encapsulates a shared_ptr to that type:

auto oneFoo = Foo::make( 2 );

And therefore you get the advantages of shared_ptr without putting references to make_shared and shared_ptr all over the code base.

Encapsulating the smart pointer type within the class provides several advantages:

  1. It lets you control the copyability and moveability of the pointer types.
  2. It hides the shared_ptr details from callers, so that non-trivial object constructions, such as those that throw exceptions, can be placed within the Instance() call.
  3. You can change the underlying smart pointer type when you're working with projects that use multiple smart pointer implementations. You could switch to a unique_ptr or even to raw pointers for a particular class, and calling code would remain the same.
  4. It concentrates the details about (smart) pointer construction and aliasing within the class that knows most about how to do it.
  5. It lets you decide which classes can use smart pointers and which classes must be constructed on the stack. The existence of the PointerType field provides a hint to callers about what types of pointers can be created that correspond for the class. If there is no PointerType defined for a class, this would indicate that no pointers to that class may be created; therefore that particular class must be created on the stack, RAII style.

However, I see no obvious way of applying this bit of code to all the classes in my project without typing the requisite typedef and static PointerType Instance() functions directly. I suspect there should be some consistent, C++11 standard, cross-platform way of doing this with policy-based templates, but a bit of experimentation has not turned up an obvious way of applying this trivially to a bunch of classes in a way that compiles cleanly on all modern C++ compilers.

Can you think of an elegant way to add these concepts to a bunch of classes, without a great deal of cutting and pasting? An ideal solution would conceptually limit what types of pointers can be created for which types of classes (one class uses shared_ptr and another uses raw pointers), and it would also handle instancing of any supported type by its own preferred method. Such a solution might even handle and/or limit coercion, by failing appropriately at compile time, between non-standard and standard smart and dumb pointer types.

like image 675
johnwbyrd Avatar asked Mar 12 '16 04:03

johnwbyrd


2 Answers

One way is to use the curiously recurring template pattern.

template<typename T>
struct shared_factory
{
    using pointer_type = std::shared_ptr<T>;

    template<typename... Args>
    static pointer_type make(Args&&... args)
    {
        return std::make_shared<T>(std::forward<Args>(args)...);
    }
};

struct foo : public shared_factory<foo>
{
    foo(char const*, int) {}
};

I believe this gives you what you want.

foo::pointer_type f = foo::make("hello, world", 42);

However...

I wouldn't recommend using this approach. Attempting to dictate how users of a type instantiate the type is unnecessarily restrictive. If they need a std::shared_ptr, they can create one. If they need a std::unique_ptr, they can create one. If they want to create an object on the stack, they can. I see nothing to be gained by mandating how your users' objects are created and managed.

To address your points:

  1. It lets you control the copyability and moveability of the pointer types.

Of what benefit is this?

  1. It hides the shared_ptr details from callers, so that non-trivial object constructions, such as those that throw exceptions, can be placed within the Instance() call.

I'm not sure what you mean here. Hopefully not that you can catch the exception and return a nullptr. That would be Java-grade bad.

  1. You can change the underlying smart pointer type when you're working with projects that use multiple smart pointer implementations. You could switch to a unique_ptr or even to raw pointers for a particular class, and calling code would remain the same.

If you are working with multiple kinds of smart pointer, perhaps it would be better to let the user choose the appropriate kind for a given situation. Besides, I'd argue that having the same calling code but returning different kinds of handle is potentially confusing.

  1. It concentrates the details about (smart) pointer construction and aliasing within the class that knows most about how to do it.

In what sense does a class know "most" about how to do pointer construction and aliasing?

  1. It lets you decide which classes can use smart pointers and which classes must be constructed on the stack. The existence of the PointerType field provides a hint to callers about what types of pointers can be created that correspond for the class. If there is no PointerType defined for a class, this would indicate that no pointers to that class may be created; therefore that particular class must be created on the stack, RAII style.

Again, I disagree fundamentally with the idea that objects of a certain type must be created and managed in a certain way. This is one of the reasons why the singleton pattern is so insidious.

like image 127
Joseph Thomson Avatar answered Oct 05 '22 22:10

Joseph Thomson


I wouldn't advise adding those static functions. Among other drawbacks, they really get pretty burdensome to create and maintain when there are multiple constructors. This is a case where auto can help as well as a typedef outside the class. Plus, you can use the std namespace (but please not in the header):

class Foo
{
public:
    Foo();
    ~Foo();
    Foo( int bar = 1 );

...
}

typedef std::shared_ptr<Foo> FooPtr;

In the C++ file:

using namespace std;
auto oneFoo = make_shared<Foo>( 2 );
FooPtr anotherFoo = make_shared<Foo>( 2 );

I think you'll find this to not be too burdensome on typing. Of course, this is all a matter of style.

like image 20
Rob L Avatar answered Oct 05 '22 22:10

Rob L