Friday, April 14, 2017

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

Leave a Comment

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?

3 Answers

Answers 1

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(); } 

Answers 2

You can do that only using dynamic_cast and static type of T has to be polymorphic. Otherwise look at this code:

struct A { int a; }; struct B { int b; }; struct C : A, B {};  B *b1 = new C, *b2 = new B; 

If you try to delete by pointer to B, there is no way to know if b1 or b2 needs to be adjusted to get_allocated_ptr. One way or another you need B to be polymorphic to get pointer to most derived object.

Answers 3

What about a virtual interface that all structs inherit from, which returns the pointer at which the object was allocated? I had to make some changes to make the code compile. Both the multiple inheritance and virtual inheritance cases work:

#include <iostream> #include <type_traits> #include <cassert>  struct H { public:     void* getHeader() { return header; }     void setHeader(void* ptr) { header = ptr; } private:     void* header; };  // multiple inheritance case //struct A : public virtual H { int a;}; //struct B : public virtual H { int b;}; //struct C : A, B { };  // virtual inheritance case struct B : public virtual H { int b; }; struct C : virtual B {};  template <typename T, typename ...Args> T* custom_new(Args&&... args) {     void* ptr = malloc(sizeof(T));     T* obj = new(ptr) T(std::forward<Args>(args)...);     obj->setHeader(ptr);     return obj; }  template <typename T> void* get_allocated_ptr(T* obj) {     return obj->getHeader(); }  template <typename T> void custom_delete(T* obj) {     void* ptr = get_allocated_ptr(obj); // here //  assert(std::is_polymorphic<T>::value || ptr == obj); // had to comment     obj->~T();     free(ptr); // heap corruption if assert ^^ failed }  using namespace std; int main(int argc, char *argv[]) {     C* c = custom_new<C>(); // b != address of allocated memory     std::cout << "PTR \t\t= " << c << std::endl;     auto b = static_cast<B*>(c);     std::cout << "CAST PTR \t= " << b << std::endl;     std::cout << "ALLOCATED PTR \t= " << get_allocated_ptr(b) << std::endl;      custom_delete(b); // UB } 

You can run this with either hierarchy, and the output is something like

PTR             = 0x7f9fd4d00b90 CAST PTR        = 0x7f9fd4d00b98 ALLOCATED PTR   = 0x7f9fd4d00b90 

although in the multiple inheritance case the pointers differ by 16 bits rather than 8 (because of the two integers).

This implementation could be improved by using templates to enable custom_new and the other functions only for structs inheriting from the H interface.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment