Получить адрес выделенной памяти указателем на базовый класс для не-полиморфных типов

простое многонаследование

struct A {};
struct B {};
struct C : A, B {};

или виртуальное наследование

struct B {};
struct C : virtual B {};

Обратите внимание, что типы не являются полиморфными.

Пользовательское распределение памяти:

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

Как я могу реализовать get_allocated_ptr для не полиморфных типов? Для полиморфных типов dynamic_cast<void*> выполняется задание.

В качестве альтернативы я могу проверить, что obj является указателем на базовый класс, поскольку удаление не полиморфного объекта указателем на базовый класс - UB. Я не знаю, как это сделать или если это возможно вообще.

operator delete правильно освобождает память в таких случаях (например, VС++), хотя стандарт говорит, что это UB. Как оно работает? специфичная для компилятора функция?

Ответ 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();
}

Ответ 2

Это можно сделать только с использованием dynamic_cast, а статический тип T должен быть полиморфным. В противном случае посмотрите на этот код:

struct A { int a; };
struct B { int b; };
struct C : A, B {};

B *b1 = new C, *b2 = new B;

Если вы попытаетесь удалить указатель на B, нет способа узнать, нужно ли настраивать b1 или b2 на get_allocated_ptr. Так или иначе вам нужно B быть полиморфным, чтобы получить указатель на большинство производных объектов.

Ответ 3

Как насчет виртуального интерфейса, на который наследуются все наследуемые структуры, который возвращает указатель, на который был выделен объект? Мне пришлось внести некоторые изменения, чтобы скомпилировать код. Работа множественного наследования и виртуального наследования:

#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
}

Вы можете запустить это с помощью либо иерархии, а вывод - что-то вроде

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

хотя в случае множественного наследования указатели отличаются на 16 бит, а не на 8 (из-за двух целых чисел).

Эта реализация может быть улучшена за счет использования шаблонов для включения custom_new и других функций только для структур, наследующих от интерфейса H.