Подтвердить что ты не робот

Как оператор удаления С++ находит местоположение памяти полиморфного объекта?

Я хотел бы знать, как оператор delete определяет ячейку памяти, которая должна быть освобождена, когда ей задан указатель базового класса, который отличается от истинной ячейки памяти объекта.

Я хочу дублировать это поведение в своем собственном распределителе/​​деаллокаторе.

Рассмотрим следующую иерархию:

struct A
{
    unsigned a;
    virtual ~A() { }
};

struct B
{
    unsigned b;
    virtual ~B() { }
};

struct C : public A, public B
{
    unsigned c;
};

Я хочу выделить объект типа C и удалить его с помощью указателя типа B. Насколько я могу судить, это допустимое использование удаления оператора, и оно работает под Linux/GCC:

C* c = new C;
B* b = c;

delete b;

Интересно, что указатели "b" и "c" фактически указывают на разные адреса из-за того, как объект выложен в памяти, а оператор удаления "знает", как найти и освободить правильное расположение памяти.

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

Примечания:

4b9b3361

Ответ 1

Это явно конкретная реализация. На практике существует относительно небольшое количество разумных способов реализации вещей. Концептуально здесь есть несколько проблем:

  • Вы должны иметь возможность получить указатель на самый производный объект, то есть объект, который (концептуально) охватывает все другие типы.

    В стандартном С++ вы можете сделать это с помощью dynamic_cast:

    void *derrived = dynamic_cast<void*>(some_ptr);
    

    Что получает C* назад только от B*, например:

    #include <iostream>
    
    struct A
    {
        unsigned a;
        virtual ~A() { }
    };
    
    struct B
    {
        unsigned b;
        virtual ~B() { }
    };
    
    struct C : public A, public B
    {
        unsigned c;
    };
    
    int main() {
      C* c = new C;
      std::cout << static_cast<void*>(c) << "\n";
      B* b = c;
      std::cout << static_cast<void*>(b) << "\n";
      std::cout << dynamic_cast<void*>(b) << "\n";
    
      delete b;
    }
    

    В моей системе есть следующее:

    0x912c008
    0x912c010
    0x912c008
    
  • После этого он становится стандартной проблемой отслеживания распределения памяти. Обычно это делается одним из двух способов: a) записывать размер выделения непосредственно перед выделенной памятью, найти размер - это просто вычитание указателя, или b) записать распределения и свободную память в какой-либо структуре данных. Подробнее см. этот вопрос, который имеет хорошую ссылку.

    С glibc вы можете довольно точно запросить размер данного распределения:

    #include <iostream>
    #include <stdlib.h>
    #include <malloc.h>
    
    int main() {
      char *test = (char*)malloc(50);
      std::cout << malloc_usable_size(test) << "\n";
    }
    

    Эта информация доступна для бесплатного/удаления аналогичным образом и используется для определения того, что делать с возвращенным фрагментом памяти.

Точные сведения о реализации malloc_useable_size приведены в исходном коде libc в файле malloc/malloc.c:

(Ниже приведены легко отредактированные объяснения Колина Пламба.)

Куски памяти сохраняются с использованием метода граничного тега, поскольку описанной, например, в Knuth или Standish. (См. Статью Пола Уилсона ftp://ftp.cs.utexas.edu/pub/garbage/allocsrv.ps для опроса таких техники.) Размеры свободных кусков хранятся как в передней части каждый кусок и в конце. Это упрощает объединение фрагментированных фрагментов в большие куски очень быстро. Поля размера также содержат биты представляя, являются ли куски бесплатными или используются.

Выделенный фрагмент выглядит следующим образом:

    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of previous chunk, if allocated            | |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of chunk, in bytes                       |M|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
            |             User data starts here...                          .  
            .                                                               .  
            .             (malloc_usable_size() bytes)                      .  
            .                                                               |   
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+     
            |             Size of chunk                                     |  
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  

Ответ 2

Уничтожение указателя базового класса требует, чтобы вы реализовали виртуальный деструктор. Если вы этого не сделали, все ставки отключены.

Первый вызванный деструктор будет одним из наиболее важных объектов, определяемых виртуальным механизмом (vtable). Этот деструктор знает размер объекта! Он может вытеснить эту информацию где-нибудь или передать ее цепочке деструкторов.

Ответ 3

Его реализация определена, но один общий метод реализации заключается в том, что operator delete на самом деле вызывается деструктором (а не кодом с delete в нем), и есть скрытый параметр для деструктора, который контролирует, будет ли operator delete.

В этой реализации большинство вызовов деструктора (все явные вызовы dtor, вызовы для автоматических и статических переменных и вызовы для базовых деструкторов из производных деструкторов) будут содержать лишний скрытый аргумент arg, равный false (поэтому удаление оператора не будет называется). Однако при наличии выражения для удаления он вызывает деструктор верхнего уровня для объекта со скрытым аргументом arg. В вашем примере это будет C:: ~ C(), поэтому он будет знать, чтобы восстановить память для всего объекта

Ответ 4

Обычная реализация (теоретически, могут быть и другие, я сомневаюсь, что на практике) заключается в том, что на базовый объект есть vtable (если нет, базовые объекты не являются полиморфными и не могут использоваться для удаления). Это vtable содержит не только указатель на виртуальные функции, но также и то, что необходимо для всего RTTI, включая смещение от текущего объекта к самому выведенному.

Чтобы объяснить (возможно, существуют различия в любой реальной реализации, и я, возможно, допустил некоторые ошибки), вот что действительно используется:

struct A_VTable_Desc {
   int offset;
   void* (destructor)();
} AVTable = { 0, A::~A };

struct A_impl {
   unsigned a;
   A_VTable_Desc* vptr;
};

struct B_VTable_Desc {
   int offset;
   void* (destructor)();
} BVtable = { 0, &B::~B };

struct B_impl {
   unsigned b;
   B_VTable_Desc* __vptr;
};

A_VTable_Desc CAVtable = { 0, &C::~C_as_A };
B_VTable_Desc CBVtable = { -8, &C::~C_as_B };

struct C {
   A_impl __aimpl;
   B_impl __bimpl;
   unsigned c;
};

а конструкторы C неявно делают что-то вроде

this->__aimpl->__vptr = &CAVtable;
this->__bimpl->__vptr = &CBVtable;

Ответ 5

При компиляции оператора delete компилятор должен определить функцию "освобождения" для вызова после выполнения деструктора. Обратите внимание, что деструктор не имеет ничего общего с вызовом освобождения, но он влияет на то, как компилятор просматривает функцию дезадаптации.

В обычном случае для объекта не существует функции освобождения типа для конкретного объекта, и в этом случае используется глобальная функция освобождения и которая неявно объявляется (С++ 03 3.7.3/2):

void operator delete(void*) throw();

Обратите внимание, что эта функция даже не принимает аргумент размера. Он должен определить размер распределения, основанный только на значении указателя. Это может быть сделано путем хранения размера выделения непосредственно перед адресом (есть ли реализация, которая делает это по-другому?).

Однако, прежде чем принять решение об использовании этой функции освобождения, компилятор выполняет поиск, чтобы определить, следует ли использовать функцию освобождения от типа. Эта функция может иметь один параметр (a void*) или два параметра (a void* и a size_t).

При поиске функции удаления, если статический тип указателя, используемого в качестве операнда для delete, имеет виртуальный деструктор, тогда (С++ 03 12.5/4):

функция освобождения является той, которая найдена путем поиска в определении динамического типы виртуального деструктора

По сути, любая функция освобождения operator delete() является виртуальной для типов с виртуальным деструктором, хотя фактическая функция должна быть static (стандарт отмечает это в 12.5/7). В этом случае компилятор может передать размер объекта, если это необходимо, поскольку он имеет доступ к динамическому типу объекта (любая необходимая корректировка указателя объекта может быть найдена аналогичным образом).

Если статический тип операнда в delete является статическим, то поиск функции operator delete() deallocation следует обычным правилам. Опять же, если компилятор выбирает функцию дезадаптации, которая нуждается в параметре размера, она может это сделать, потому что она знает статический тип объекта во время компиляции.

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

Ответ 6

указатель на полиморфный объект обычно реализуется как указатель на объект и виртуальную таблицу, которая содержит информацию о базовом классе объекта. delete будет знать эти детали реализации и найти нужный деструктор

Ответ 7

Он может сделать это так же, как это делает malloc. Некоторые mallocs записывают размер, непосредственно предшествующий самому объекту. Большинство современных маллоков намного сложнее. См. tcmalloc, быстрый распределитель, который объединяет объекты одного и того же размера на страницах, так что ему нужно хранить информацию о размере только на странице детализации.