Ответ 1
У вас действительно есть более серьезная проблема, чем получение адреса полного объекта. Рассмотрим этот пример:
struct Base
{
std::string a;
};
struct Derived : Base
{
std::string b;
};
Base* p = custom_new<Derived>();
custom_delete(p);
В этом примере custom_delete фактически освободит правильный адрес (static_cast<void*>(static_cast<Derived*>(p)) == static_cast<void*>(p)
), но строка obj->~T()
вызовет деструктор для Base
, что означает, что поле b
просочилось.
Так что не делай этого
Вместо того, чтобы возвращать необработанный указатель из custom_new
, верните объект, привязанный к типу T, и который знает, как его удалить. Например:
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;
}
}
Теперь невозможно случайно удалить неправильный адрес и вызвать неправильный деструктор, потому что единственный код, который вызывает custom_free, знает полный тип вещи, которую он удаляет.
Примечание. Остерегайтесь метода unique_ptr:: reset (указатель). Этот метод чрезвычайно опасен при использовании настраиваемого делетера, поскольку на стороне вызывающего абонента есть указатель, который был назначен соответствующим образом. Компилятор не может помочь, если метод вызывается с недопустимым указателем.
Прохождение вокруг базовых указателей
Возможно, вы хотите, чтобы оба передавали базовый указатель на функцию и предоставляли эту функцию для освобождения объекта. В этом случае вам нужно использовать стирание типа, чтобы скрыть тип объекта от потребителей, сохраняя при этом знание своего самого производного типа внутри. Самый простой способ сделать это - std::shared_ptr
. Например:
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) };
Теперь вы можете свободно перемещаться по shared_base
, и когда окончательная ссылка будет отпущена, полный объект Derived
будет уничтожен и его правильный адрес будет передан на custom_free
. Если вам не нравится семантика shared_ptr
, довольно просто создать указатель стирания типа с семантикой unique_ptr
.
Примечание. Недостатком этого подхода является то, что shared_ptr требует отдельного распределения для своего блока управления (который не будет использовать custom_malloc
). С небольшим количеством работы вы можете обойти это. Вам нужно создать настраиваемый распределитель, который обертывает custom_malloc
и custom_free
, а затем используйте std::allocate_shared
для создания ваших объектов.
Полный рабочий пример
#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 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();
}