1

I'm implementing a memory pool in C++11. All objects stored in the memory pool are required to inherit from the MemoryPoolObject class, but they may use multiple inheritance.

When I allocate a new object, I use malloc to create it, and then use the placement constructor - which I can do, because I know it's type (allocate() is a template function that takes the type as a template argument). I also store its size on the object on it (as a member variable of the MemoryPoolObject class).

When I deallocate the object, I want to call its destructor. but I no longer know the object type. I do know that it derives from MemoryPoolObject, which has a virtual destructor. So... if I make an explicit call to ~MemoryPoolObject(), will that do the right thing (including calling into the destructors of any multiply inherited base classes)?

Here's some code. It doesn't show how the unused objects get stored and retrieved in the pool. but that's really not relevant to the question at hand.

class BaseClass
{
public:
    virtual ~BaseClass();
    ...
};

class MemoryPoolObject
{
public:
    virtual ~MemoryPoolObject();

    // In reality I don't just expose this, but for simplicity...
    size_t m_ObjectSize;
};

class ChildClass : public BaseClass, public MemoryPoolObject
{
    virtual ~ChildClass();
    ...
};

// allocation (simplified)
template<class T> 
T* allocate()
{
    size_t objectSize = sizeof(T);
    T* obj = (T*)malloc(objectSize);
    new(obj) T();
    obj->m_ObjectSize = objectSize;
}

// deallocation (also simplified)
void deallocate(MemoryPoolObject* obj)
{
    // Does this call ~BaseClass() and ~ChildClass()?
    obj->~MemoryPoolObject();
}
PrimeOfKnights
  • 426
  • 5
  • 17
Kevin Dill
  • 186
  • 1
  • 10
  • 2
    Use [smart pointers](http://en.cppreference.com/w/cpp/memory/shared_ptr). Abandon that odd `malloc` - `new` combo. – Ron Aug 29 '17 at 21:40
  • 1
    If I didn't want to manage memory, I'd work in JavaScript. :) I did find this question... but as far as I can see it doesn't get at the multiple inheritance aspect of the problem... https://stackoverflow.com/questions/1036019/does-calling-a-destructor-explicitly-destroy-an-object-completely – Kevin Dill Aug 29 '17 at 21:41
  • Abandon multiple inheritance too. – Ron Aug 29 '17 at 21:43
  • 1
    cant you use this->~MemoryPoolObject(); Or am I misunderstanding – Charlie Aug 29 '17 at 21:45
  • If you have wandered down a path where you feel the need to explicitly call destructors then I'll wager real money that you are *doing it wrong*. – Jesper Juhl Aug 29 '17 at 22:18
  • @JesperJuhl I'll take that bet. Mostly in library code, but there are some genuine cases where you can't get around without explicitly calling the destructor. Especially when you need to separate memory allocation (which can be automatic storage duration) and object construction. Just take a look at `std::optional`. Granted those cases should not be in user code, but in library. – bolov Aug 29 '17 at 22:33
  • The destructor is virtual so you can call it on any ancestor class, but there's little point if you don't free any memory. – n. m. could be an AI Aug 29 '17 at 22:46
  • There is absolutely no benefit to using `malloc`+`new` in this manner. You are duplicating what the non-placement `new` already does, so just have `allocate()` use `new` normally: `template T* allocate() { T* obj = new T(); obj->m_ObjectSize = sizeof(T); return obj; }`, and then have `deallocate()` use `delete` normally (especially since you are leaking the `malloc`ed memory): `void deallocate(MemoryPoolObject* obj) { delete obj; }`. You should only use `placement-new` when allocating your own memory buffers more efficiently then you are doing here. – Remy Lebeau Aug 30 '17 at 00:52
  • No, I'm not doing it wrong. :) What my post doesn't show is that deallocate actually saves the memory block, and allocate looks for previously deallocated blocks before malloc-ing a new one. This means that I can allocate and deallocate as often as I like without trashing the heap... which will make a lot of other code more straightforward. – Kevin Dill Aug 30 '17 at 14:45

1 Answers1

6

So... if I make an explicit call to ~MemoryPoolObject(), will that do the right thing (including calling into the destructors of any multiply inherited base classes)?

Yes, it will.

However, I think you can change strategy a bit to make your code more intuitive.

Overload the operator new and operator delete in MemoryPoolObject and let users use operator new and operator delete in the usual way.

An example program:

#include <iostream>
using namespace std;

void* allocate(size_t s)
{
   // This is a simple implementation.
   // To use a memory pool for allocating objects, this function
   // and deallocate() need to be changed accordingly.

   std::cout << "Allocating memory of size " << s << std::endl;
   return new char[s];
}

void deallocate(void* ptr, size_t s)
{
   std::cout << "Dellocating memory of size " << s << std::endl;
   delete [] static_cast<char*>(ptr);
}

class MemoryPoolObject
{
   public:

      virtual ~MemoryPoolObject() {}

      void* operator new (size_t s)
      {
         return allocate(s);
      }

      void operator delete (void* ptr, size_t s)
      {
         return deallocate(ptr, s);
      }
};

class BaseClass
{
   public:
      virtual ~BaseClass() {}
};

class ChildClass : public BaseClass, public MemoryPoolObject
{
   virtual ~ChildClass() {}
};

int main()
{
   MemoryPoolObject* ptr = new ChildClass;
   delete ptr;
}

Output

Allocating memory of size 16
Dellocating memory of size 16
R Sahu
  • 204,454
  • 14
  • 159
  • 270
  • In terms of the OP's original example, your `allocate()` and `deallocate()` could use `malloc()`/`free()` instead of `new[]`/`delete[]`, eg: `void* allocate(size_t s) { ... return malloc(s); } void deallocate(void* ptr, size_t s) { ... free(ptr); }` Neither approach is particularly good, though, since you are not providing any new functionality that the standard `new`/`delete` don't already provide. This approach is better suited for when `allocate()` can grab existing memory from a pool and then `deallocate()` can put the memory back in the pool. – Remy Lebeau Aug 30 '17 at 00:56
  • @RemyLebeau, agreed. I am assuming the OP will add the necessary code to use a memory pool for managing dynamically allocated memory. – R Sahu Aug 30 '17 at 03:13
  • Very nice! I was stressing out a bit about how to enforce that you don't allocate a memory pool object using my allocator and then delete it using delete (or vice versa)... this is definitely cleaner. And yes, I'll be hooking it up to a memory pool. I just didn't include that in my original post because it was long enough already. :) – Kevin Dill Aug 30 '17 at 14:43
  • So I tried this. In VS2013, I'm finding that sometimes my class-specific overrides for new and delete get called... and sometimes they do not. It's not at all clear to me why they would or would not - I've spent the last 4 hours beating on it, trying to narrow it down, with no luck. Looking at usage stats, about 2/3 of the pointers created in allocate() are not released via deallocate(). – Kevin Dill Aug 30 '17 at 19:37
  • @KevinDill, One thing that could lead to such behavior would be if you called `delete` on a pointer that is not a direct sub-type of `MemoryPoolObject`. E.g. `BaseClass* bPtr = new ChildClass; delete bPtr;`. Since `BaseClass` is not a sub-type of `MemoryPoolObject` the overloaded `operator delete` won't be called. – R Sahu Aug 30 '17 at 22:10
  • @RSahu, So I've tried this with and without multiple inheritance. In the real code, an object "Foo" will typically inherit from a modular interface (call it FooBase), which inherits from ComponentBase which inherits from LoadableObject. There are some loadable objects that aren't components. I've tried having either ComponentBase or LoadableObject inherit from MemoryPoolObject, and I run into many cases of the overloaded new or delete not being called either way. I also never have a LoadableObject*, though I do sometimes have a ComponentBase*. I'm actually wondering if it's a VS2013 bug... – Kevin Dill Aug 31 '17 at 23:16
  • @KevinDill, I am not sure what could be wrong. Good luck with getting it to work. – R Sahu Sep 01 '17 at 03:11
  • @RSahu, just wanted to follow up... I finally figured this out. Turns out to have been a bug in my instrumentation - overloading new and delete does work as advertised. Proving once again that if i find myself thinking that it might be a compiler bug... odds are that I'm wrong. ;) Thanks again for an outstanding response! – Kevin Dill Sep 06 '17 at 17:45
  • @KevinDill, glad to hear it. – R Sahu Sep 06 '17 at 17:46