0

In environment that has many custom heap allocators, is it generally required that address of original void* need to be cached within the custom smart pointer?

Example

To identify the allocator (Allo) of a certain memory block content later :-
When the block is allocated, I think it is necessary to store a hacky infomation (meta-data and Allo*) near it.

enter image description here

When I want to deallocated a void*, I can minus the void* pointer to find the Allo* e.g.

template<class T>class StrongPointer{
    public: void* content;
    public: ~StrongPointer(){
        Allo* allo=static_cast<Allo*>((void*)(static_cast<char*>(content)-4));  
        //^ 32-bit system
        allo->deallocate(content);
    }
    public: void get(){ return static_cast<T*>(content); }
}

It should work.

However, this thing will break when I want it to support casting StrongPointer<Derived> -> StrongPointer<Base2>.
(According to C++ virtual table layout of MI(multiple inheritance))

class Base1{/*some fields */};
class Base2{/*some fields */};
class Derived : public Base1,public Base2{};

For example, the result of casting StrongPointer<Derived> to StrongPointer<Base2> will has StrongPointer<Base2>::content that doesn't directly next-to the location of Allo* anymore.

enter image description here

template<class T1,class T2> StrongPointer<T2> cast(StrongPointer<T1>&& t1){
    StrongPointer<T2> r;
    r.content=static_cast<T2*>(t1.get());
    //^ location change, so "content-(4 bytes)" doesn't point to Allo* anymore
    return r;
} 

Question

In my opinion, there are some workarounds:-

  • store Allo* inside every strong pointer. OR
  • store offset which +/- in every casting OR
  • store Allo* + 1 which reflect the real address of allocated content

It all boils down to :-
Do I really have to store another variable inside Strong_Pointer<T>?

javaLover
  • 6,347
  • 2
  • 22
  • 67
  • 1
    If you always have a virtual dtor then no you don't have to, you can always recover the originalblock from a custom operator delete you define per allocated class. If you don't have a virtual dtor you have to store a custom deleter anyway. – n. m. could be an AI May 30 '17 at 05:02
  • @n.m. Thank, sir n.m. .... `a custom operator delete` = destructor of `Base2`? How to get the original block if it has the operator? Do you mean I should retrieve such information from v-table? How? .... I understand the part about custom deleter (function pointer), but I think there should be an alternative. – javaLover May 30 '17 at 05:06
  • Overloaded `operator delete`, not destructor. It gets the same void pointer you return from your overloaded `operator new` which gets your menory block from your custom allocator. If you add the size of control block in operator new, you subtract it in operator delete. – n. m. could be an AI May 30 '17 at 05:14
  • @n.m. Roughly speaking, I have to overload [`operator delete (void *p)`](https://stackoverflow.com/a/14819931) for many classes `T`. ... It is quite intrusive, so it is not a generally-recommended approach, and function pointer is a more common practice. ... Do I understand it correctly? – javaLover May 30 '17 at 05:25
  • If you find overloading `delete` intrusive, just use a custom deleter – Passer By May 30 '17 at 06:22

2 Answers2

3

Here's how one can organise this without being intrusive and without having to use fat pointers like shared_ptr (If fat pointers are OK just use shared_ptr, I assume you want to avoid them). This example doesn't use custom smart pointers, just any kind of pointer will do. The only thing you need to remember is to use the "make" function whenever you want to use a custom allocator.

#include <cstdlib>
#include <cstddef>
#include <iostream>
#include <memory>
#include <cstring>

// The metadata

struct Alloc;

struct UnalignedControlBlock
{
    int magic1;
    Alloc* allocator;
    std::size_t size;
    char magic2[19];
};

union ControlBlock
{
    UnalignedControlBlock ucb;
    std::max_align_t aligner;
};

// An allocator

struct Alloc
{
    static Alloc global_allocator;

    static void* allocate(std::size_t size)
    {
        void* p = ::operator new (size + sizeof(ControlBlock));
        ControlBlock* cb = static_cast<ControlBlock*>(p);
        cb->ucb.allocator = &global_allocator;
        cb->ucb.magic1 = 42;
        cb->ucb.size = size;
        std::strcpy(cb->ucb.magic2, "Hey there!");
        std::cout << "Allocate: block: " << cb << " size: " << size << " magic1: " << cb->ucb.magic1 << " magic2: " << cb->ucb.magic2 << " allocator: " << cb->ucb.alloca
        return cb+1;
    }

    static void deallocate (void* p)
    {
        ControlBlock* cb = static_cast<ControlBlock*>(p);
        cb--;
        std::cout << "Deallocate: block: " << cb << " size: " << cb->ucb.size << " magic1: " << cb->ucb.magic1 << " magic2: " << cb->ucb.magic2 << " allocator: " << cb->u
        ::operator delete (cb);
    }
};

// The holder of the custom new and delete operators (where the magic happens)

template <class T>
struct Allocated : T
{
    template <class ... Arg>
        Allocated(Arg ... arg) : T(arg ...) {}
    void* operator new (size_t size) { return Alloc::allocate(size); }
    void  operator delete (void* p) { return Alloc :: deallocate(p); }
};

// The make function (return your own smart pointer)

template <class T, class ... Args>
std::unique_ptr<T> make_smart (Args ... args)
{
    return std::unique_ptr<T>(new Allocated<T>(args...));
};

// Test drive

struct Test1
{
    const int filler = 42;
    virtual ~Test1() {
       std::cout << "Test1::~Test1 " << this << " " << filler << std::endl;
    }
};

struct Test2 : virtual Test1
{
    const int filler = 43;
    virtual ~Test2() {
       std::cout << "Test2::~Test2 " << this << " " << filler << std::endl;
    }
};

struct Test3 : virtual Test1
{
    const int filler = 44;
    virtual ~Test3() {
       std::cout << "Test3::~Test3 " << this << " " << filler << std::endl;
    }
};

struct Test4 : Test2, Test3
{
    const int filler = 45;
    virtual ~Test4() {
       std::cout << "Test4::~Test4 " << this << " " << filler << std::endl;
    }
};


Alloc Alloc::global_allocator;

int main ()
{
    std::unique_ptr<Test1> p1 = make_smart<Test1>();
    std::unique_ptr<Test1> p2 = make_smart<Test2>();
    std::unique_ptr<Test1> p3 = make_smart<Test3>();
    std::unique_ptr<Test1> p4 = make_smart<Test4>();
}

Test output

Allocate: block: 0x1817c20 size: 16 magic1: 42 magic2: Hey there! allocator: 0x6052e9
Allocate: block: 0x1818080 size: 32 magic1: 42 magic2: Hey there! allocator: 0x6052e9
Allocate: block: 0x18180e0 size: 32 magic1: 42 magic2: Hey there! allocator: 0x6052e9
Allocate: block: 0x1818140 size: 48 magic1: 42 magic2: Hey there! allocator: 0x6052e9
Test4::~Test4 0x1818170 45
Test3::~Test3 0x1818180 44
Test2::~Test2 0x1818170 43
Test1::~Test1 0x1818190 42
Deallocate: block: 0x1818140 size: 48 magic1: 42 magic2: Hey there! allocator: 0x6052e9
Test3::~Test3 0x1818110 44
Test1::~Test1 0x1818120 42
Deallocate: block: 0x18180e0 size: 32 magic1: 42 magic2: Hey there! allocator: 0x6052e9
Test2::~Test2 0x18180b0 43
Test1::~Test1 0x18180c0 42
Deallocate: block: 0x1818080 size: 32 magic1: 42 magic2: Hey there! allocator: 0x6052e9
Test1::~Test1 0x1817c50 42
Deallocate: block: 0x1817c20 size: 16 magic1: 42 magic2: Hey there! allocator: 0x6052e9

This illustrates correct metadata recovery from pointers adjusted for inheritance. Custom deleter is not necessary if the destructor is virtual.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
2

You should use the standard shared_ptr for that. When you create a new instance of a shared_ptr you can define the deleter function (or object) that will be called to delete that memory.
Another neat feature is that the deleter class is kept when you cast to shared_ptr<void>, so the shared_ptr mechanism can handle the delete properly from void pointers as well.

You can have a templated deleter by type, but that's just implementation nuances.

Here's an example of a deleter that (just for fun) overrides the deletion altogether and doesn't delete the object at all.

#include <iostream>
#include <memory>

class Myclass
{
public:
    ~Myclass()
    {
        std::cout << "Myclass Destructor" << std::endl;
    }
};

struct MyDeleter {
    void operator()(Myclass* p) const {
        std::cout << "In custom destructor" << std::endl;
        //delete p;
    }
};


int main()
{
    {
        std::shared_ptr<Myclass> example1(new Myclass(), MyDeleter());
        std::cout << "Delete void example1 at end of scope, with no actual deletion" << std::endl;
    }
    {
        std::shared_ptr<Myclass> example2 = std::make_shared<Myclass>();
        std::cout << "Delete example2 at end of scope" << std::endl;
    }
    {
        std::shared_ptr<void> example3 = std::static_pointer_cast<void>(std::make_shared<Myclass>());
        std::cout << "Delete void example3 at end of scope" << std::endl;
    }

    return 0;
}

output:

Delete void example1 at end of scope, with no actual deletion  
In custom destructor  
Delete example2 at end of scope  
Myclass Destructor  
Delete void example3 at end of scope  
Myclass Destructor  

So if you insist of reinventing the wheel, you can look at the shared_ptr implementation of a well thought-of wheel.

Yochai Timmer
  • 48,127
  • 24
  • 147
  • 185
  • I will suffer function-pointer cost, correct? Therefore, storing `Allo*` directly might be a better idea. I know that the difference might be trivial, but I would like to know. – javaLover May 30 '17 at 08:03
  • It's equivalent to your implementation. There's a get() method to get the pointer. And there might be one extra function call when calling the destructor, and redirecting to the deleter. But that's implementation specific, and most compilers will opt it out. – Yochai Timmer May 30 '17 at 08:08
  • @YochaiTimmer shared_ptr is usually twice as large as a normal pointer, that's why it is so versatile. – n. m. could be an AI May 30 '17 at 08:11
  • @n.m. The actual size is implementation specific. you have a reference counter, and some extra info (which most is just template generated). You could use unique_ptr with a deleter if you don't need reference counting. Remember that adding a virtual table to a class will increase its size as well (at least a size of a pointer more) – Yochai Timmer May 30 '17 at 08:18
  • @YochaiTimmer Many things are in theory implementation specific, in practice there's often a single reasonable way of doing stuff, and in this case it's is keeping two pointers in a `shared_ptr`. Try to come up with an alternative way. If you are using public inheritance without virtual functions you are doing it wrong anyway, but even if you for some reason consider this, an extra pointer-per-smart-pointer is not quite the same as an extra pointer-per-object. You normally have several pointers pointing to the same object, otherwise there's no point in having them *shared*. – n. m. could be an AI May 30 '17 at 08:50
  • @n.m. Actually i wasn't arguing. I was stating it's implementation specific, and I think it's actually more than that. There's the reference counter, and the pointer to that reference counter at least. And about the extra virtual table pointer - That changes the size of the class itself and adds a pointer which isn't serializeable. When having custom allocators, it's usually do to HW constraints, where we wouldn't want to change the objects themselves, but wouldn't mind adding extra memory outside of those pools. – Yochai Timmer May 30 '17 at 08:58