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.
0 comments:
Post a Comment