Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get address of allocated memory by pointer to a base class for non-polymorphic types

simple multi-inheritance

struct A {};
struct B {};
struct C : A, B {};

or virtual inheritance

struct B {};
struct C : virtual B {};

Please note types are not polymorphic.

Custom memory allocation:

template <typedef T, typename... Args>
T* custom_new(Args&& args...)
{
    void* ptr = custom_malloc(sizeof(T));
    return new(ptr) T(std::forward<Args>(args)...);
}

template <typedef T>
void custom_delete(T* obj)
{
    if (!obj)
        return obj;

    void* ptr = get_allocated_ptr(obj); // here
    assert(std::is_polymorphic_v<T> || ptr == obj);
    obj->~T();
    custom_free(ptr); // heap corruption if assert ^^ failed
}

B* b = custom_new<C>(); // b != address of allocated memory
custom_delete(b); // UB

How can I implement get_allocated_ptr for non polymorphic types? For polymorphic types dynamic_cast<void*> does the job.

Alternatively I could check that obj is a pointer to a base class as deleting a non polymorphic object by a pointer to base class is UB. I don't know how to do this or if it's possible at all.

operator delete properly deallocates memory in such cases (e.g. VC++), though standard says it's UB. How does it do this? compiler-specific feature?

like image 969
Andriy Tylychko Avatar asked Apr 05 '17 08:04

Andriy Tylychko


1 Answers

You actually have a more serious problem than getting the address of the full object. Consider this example:

struct Base
{
  std::string a;
};

struct Derived : Base
{
  std::string b;
};

Base* p = custom_new<Derived>();
custom_delete(p);

In this example, custom_delete will actually free the correct address (static_cast<void*>(static_cast<Derived*>(p)) == static_cast<void*>(p)), but the line obj->~T() will invoke the destructor for Base, meaning that the b field is leaked.


So Don't Do That

Instead of returning a raw pointer from custom_new, return an object that is bound to the type T and that knows how to delete it. For example:

template <class T> struct CustomDeleter
{
  void operator()(T* object) const
  {
    object->~T();
    custom_free(object); 
  }
};

template <typename T> using CustomPtr = std::unique_ptr<T, CustomDeleter<T>>;

template <typename T, typename... Args> CustomPtr<T> custom_new(Args&&... args)
{
  void* ptr = custom_malloc(sizeof(T));
  try
  {
    return CustomPtr<T>{ new(ptr) T(std::forward<Args>(args)...) };
  }
  catch (...)
  {
    custom_free(ptr);
    throw;
  }
}

Now it's impossible to accidentally free the wrong address and call the wrong destructor because the only code that calls custom_free knows the complete type of the thing that it's deleting.

Note: Beware of the unique_ptr::reset(pointer) method. This method is extremely dangerous when using a custom deleter since the onus is on the caller to supply a pointer that was allocated in the correct way. The compiler can't help if the method is called with an invalid pointer.


Passing Around Base Pointers

It may be that you want to both pass a base pointer to a function and give that function responsibility for freeing the object. In this case, you need to use type erasure to hide the type of the object from consumers while retaining knowledge of its most derived type internally. The easiest way to do that is with a std::shared_ptr. For example:

struct Base
{
  int a;
};

struct Derived : Base
{
  int b;
};

CustomPtr<Derived> unique_derived = custom_new<Derived>();

std::shared_ptr<Base> shared_base = std::shared_ptr<Derived>{ std::move(unique_derived) };

Now you can freely pass around shared_base and when the final reference is released, the complete Derived object will be destroyed and its correct address passed to custom_free. If you don't like the semantics of shared_ptr, it's fairly straightforward to create a type erasing pointer with unique_ptr semantics.

Note: One downside to this approach is that the shared_ptr requires a separate allocation for its control block (which won't use custom_malloc). With a little more work, you can get around that. You'd need to create a custom allocator that wraps custom_malloc and custom_free and then use std::allocate_shared to create your objects.


Complete Working Example

#include <memory>
#include <iostream>

void* custom_malloc(size_t size)
{
  void* mem = ::operator new(size);
  std::cout << "allocated object at " << mem << std::endl;
  return mem;
}

void custom_free(void* mem)
{
  std::cout << "freeing memory at " << mem << std::endl;
  ::operator delete(mem);
}

template <class T> struct CustomDeleter
{
  void operator()(T* object) const
  {
    object->~T();
    custom_free(object); 
  }
};

template <typename T> using CustomPtr = std::unique_ptr<T, CustomDeleter<T>>;

template <typename T, typename... Args> CustomPtr<T> custom_new(Args&&... args)
{
  void* ptr = custom_malloc(sizeof(T));
  try
  {      
    return CustomPtr<T>{ new(ptr) T(std::forward<Args>(args)...) };
  }
  catch (...)
  {
    custom_free(ptr);
    throw;
  }
}

struct Base
{
  int a;
  ~Base()
  {
    std::cout << "destroying Base" << std::endl;
  }
};

struct Derived : Base
{
  int b;
  ~Derived()
  {
    std::cout << "detroying Derived" << std::endl;
  }
};

int main()
{
  // Since custom_new has returned a unique_ptr with a deleter bound to the
  // type Derived, we cannot accidentally free the wrong thing.
  CustomPtr<Derived> unique_derived = custom_new<Derived>();

  // If we want to get a pointer to the base class while retaining the ability
  // to correctly delete the object, we can use type erasure.  std::shared_ptr
  // will do the trick, but it's easy enough to write a similar class without
  // the sharing semantics.
  std::shared_ptr<Base> shared_base = std::shared_ptr<Derived>{ std::move(unique_derived) };

  // Notice that when we release the shared_base pointer, we destroy the complete
  // object.
  shared_base.reset();
}
like image 97
Peter Ruderman Avatar answered Nov 05 '22 15:11

Peter Ruderman