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?
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.
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.
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.
#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();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With