Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stop heap allocation via make_shared

Tags:

c++

c++11

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?

like image 332
JaredC Avatar asked Dec 25 '12 01:12

JaredC


People also ask

Why is make_shared more efficient?

One reason is because make_shared allocates the reference count together with the object to be managed in the same block of memory.

Why should I use make_shared?

Using make_shared makes a difference because it could prolong the life-time of the memory allocated for the managed object. When the shared_ptr count hits 0, the destructor for the managed object gets called regardless of make_shared , but freeing its memory can only be done if make_shared was not used.

What is make_shared?

make_shared is exception-safe. It uses the same call to allocate the memory for the control block and the resource, which reduces the construction overhead. If you don't use make_shared , then you have to use an explicit new expression to create the object before you pass it to the shared_ptr constructor.

What is boost :: make_shared?

The advantage of boost::make_shared() is that the memory for the object that has to be allocated dynamically and the memory for the reference counter used by the smart pointer internally can be reserved in one chunk.


2 Answers

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).

like image 184
Ben Voigt Avatar answered Oct 31 '22 20:10

Ben Voigt


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.

like image 28
Dietmar Kühl Avatar answered Oct 31 '22 21:10

Dietmar Kühl