2

I have some classes that are only ever used via std::shared_ptr. Instances of these classes are not designed to be used directly by allocating them on the stack or by raw pointers via new. I enforce this currently by making the constructors protected and having a static member function that actually does object instantiation and returns a shared_ptr to the object:

class Example {
protected:
    Example() { }
public:
    static std:shared_ptr<Example> create() { return std::shared_ptr<Example>(new Example()); }
};

I realize this isn't bullet proof, as you can still call get() on the shared_ptr, but it seems reasonable as an indication of support usage.

However, I can't use std::make_shared() as the constructor is protected, and I know there are memory allocation/performance advantages to make_shared().

Is the above bad practice, or is there a way of using make_shared() without making the constructors public?

Remy Lebeau
  • 454,445
  • 28
  • 366
  • 620
  • 1
    This is questionable design. Why class is designed in such a way that it forces it's user to allocate it through some specific facility? – SergeyA Oct 26 '20 at 19:25
  • Could you explain why `Instances of these classes are not designed to be used directly by allocating them on the stack or by raw pointers via new`? This might help to give you a solution to what you try to achieve/solve. – t.niese Oct 26 '20 at 19:49
  • @remy that is sadly a C++11 answer, and C++17 changes best practices since then. – Yakk - Adam Nevraumont Oct 26 '20 at 20:03

1 Answers1

1

There is on old trick to give limited permission to another function to make an object; you pass a token.

struct Example_shared_only {
private:
  // permission token.  explicit constructor ensures
  // you have to name the type before you can create one,
  // and only Example_shared_only members and friends can
  // name it:
  struct permission_token_t {
    explicit permission_token_t(int) {}
  };
public:
  // public ctor that requires special permission:
  Example_shared_only( permission_token_t ) {}
  // delete special member functions:
  Example_shared_only()=delete;
  Example_shared_only(Example_shared_only const&)=delete;
  Example_shared_only(Example_shared_only &&)=delete;
  Example_shared_only& operator=(Example_shared_only const&)=delete;
  Example_shared_only& operator=(Example_shared_only &&)=delete;
  // factory function:
  static std::shared_ptr<Example_shared_only>
  make_shared() {
    return std::make_shared<Example_shared_only>( permission_token_t(0) );
  }
};

now Example_shared_only::make_shared() returns a shared_ptr that was created with make_shared, and nobody else can do much with it.

If you have access to more modern dialects of C++ we can do better than that:

template<class F>
struct magic_factory {
  F f;
  operator std::invoke_result_t<F const&>() const { return f(); }
};

struct Example2 {
  static std::shared_ptr<Example2> make() {
    return std::make_shared<Example2>( magic_factory{ []{ return Example2{}; } } );
  }
private:
  Example2() = default;
};

Live example.

This requires for guaranteed elision.

magic_factory can be cast into whatever your factory function produces, and with guaranteed elision builds that object in-place. It has fancier uses in other contexts, but here it lets you export the constructor to make shared.

The lambda passed to magic_factory is an implicit friend of Example2, which gives it access to the private ctor. Guaranteed elision means a function with the signature ()->T can be called to create a T "in place" without any logical copies.

make_shared<T> attempts to construct its T using the argument. C++ examines operator T while this happens; our magic_factory has one such operator T. So it is used.

It does something like

::new( (void*)ptr_to_storage ) Example2( magic_factory{ lambda_code } )

(If you are unfamiliar, this is known as "placement new" -- it states "please build an Example2 object at the location pointed to by ptr_to_storage).

The beauty of guaranteed elision basically passes into the lambda_code the address where the Example2 is being created (aka ptr_to_storage), and the object is constructed right there.

Yakk - Adam Nevraumont
  • 235,777
  • 25
  • 285
  • 465