JaredC
JaredC

Reputation: 5300

Stop heap allocation via make_shared

I want to force my object to be on the stack to enforce very strict semantics and address some lifetime concerns. I've read a couple articles on how to do this, and arrived at making operator new private (or deleted). This seems to work as expected when new is used directly, but make_shared compiles fine.

#include <boost/smart_ptr.hpp>

class A
{
private:
   void *operator new( size_t );
   void operator delete( void* );
   void *operator new[]( size_t );
   void operator delete[]( void* );
};

int main()
{
//  A* a = new A;      // Correctly produces compile error
    boost::shared_ptr<A> a2 = boost::make_shared<A>();
}

Using new A directly gives me this error as expected:

error: ‘static void* A::operator new(size_t)’ is private

I am guessing that make_shared is working because it is using the placement new operator, but I couldn't find any articles that discussed how to prohibit this. The best solution I've come up with is to explicitly delete a template specialization for make_shared

namespace boost
{
    template<>
    shared_ptr<A> make_shared<A>() = delete;
};

This is obviously very specific to boost::make_shared though. Is this really the best way?

Upvotes: 6

Views: 1514

Answers (2)

Dietmar K&#252;hl
Dietmar K&#252;hl

Reputation: 153820

You cannot enforce that objects of a class are always on the stack by making any operators inaccessible only: Any object which can be constructed on the stack can also be embedded as a member into another object. Even though your original class might struggle against being allocated on the heap the containing class won't. I'd think this is what happens in the case of boost::make_shared(): Internally it probably allocates some record containing both its administration data plus the object actually being allocated. Alternatively, it may use allocation function from some sort of allocator which don't map to the type's operator new() but use its own operator new() overload instead.

I'm not sure if it is possible to prevent heap allocations (at least, when the object is embedded into another object) but any approach doing so would need to make the constructors inaccessible (most likely private) and use some sort of factory functions, possibly in combination with moving. On the other hand, if you can move an object you have an accessible constructor and nothing prevents the object from being moved into an object on the heap.

If you specifically want to prevent the use of std::make_shared() for a concrete type (or boost::make_shared() although I can't quote the rules for specializing the latter), you can specialize std::make_shared(): According to 17.6.4.2.1 [namespace.std] paragraph 1 a user is allowed to specialize any template (unless otherwise specified) if it involves a user-defined type. Thus, you can prevent A from being used with std::make_shared():

class A
{
public:
    A();
    A(int);
};

namespace std
{
    template <> std::shared_ptr<A> make_shared<A>() = delete;
    template <> std::shared_ptr<A> make_shared<A, int>(int&&) = delete;
}
namespace boost
{
    template <> boost::shared_ptr<A> make_shared<A>() = delete;
    template <> boost::shared_ptr<A> make_shared<A, int>(int&&) = delete;
}

Obviously, if you have multiple constructors in A you might need to add more specializations. ... and if your type happens to be a class template or your constructor to be a template, you'll be out of luck: You cannot partially specialize function templates.

With respect to your question about placement new (which may or may not be used by make_shared()): The placement new (and delete) signatures are these:

void* operator new(size_t, void*) noexcept;
void* operator new[](size_t, void*) noexcept;
void  operator delete(void*, void*) noexcept;
void  operator delete[](void*, void*) noexcept;

(see 18.6 [support.dynamic] paragraph 1). I doubt that making them inaccessible will help you anything, though.

Upvotes: 2

Ben Voigt
Ben Voigt

Reputation: 283634

Placement forms of new are pretty easy to handle -- they just come with extra arguments. For example, the simple placement form is

void* operator new(std::size_t, void*);

Note that 18.6.1.3 prohibits redefining these at global scope; however there should be no problem with redefining (or deleting/making inaccessible) them for your particular type.

Unfortunately, make_shared uses scoped ::new (pv) T(std::forward<Args>(args)...). And as I mentioned, you aren't allowed to mess with the global placement new. So you can't prevent it at compile-time, and any runtime trap would be a hack (checking the this pointer to see whether it's in-bounds of the stack).

Upvotes: 4

Related Questions