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

Распределители пулов с виртуальными деструкторами

Я работаю над старой базой кода С++ 03. Один раздел выглядит примерно так:

#include <cstddef>

struct Pool
{ char buf[256]; };

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

struct B : A
{
  static void *operator new(std::size_t s, Pool &p) { return &p.buf[0]; }
  static void operator delete(void *m, Pool &p) { } // Line D1
  static void operator delete(void *m) { delete m; } // Line D2
};

Pool p;

B *doit() { return new(p) B; }

То есть, B происходит от A, но экземпляры B выделяются из пула памяти.

(Обратите внимание, что этот пример немного упрощен... На самом деле распределитель пулов делает что-то нетривиальным, поэтому требуется размещение operator delete в строке D1.)

В последнее время мы включили больше предупреждений о компиляторах, а в строке D2 появляется следующее предупреждение:

предупреждение: удаление 'void * - undefined [-Wdelete-incomplete]

Хорошо, да, очевидно. Но поскольку эти объекты всегда выделены из пула, я решил, что нет необходимости в настраиваемом (не размещении) operator delete. Поэтому я попытался удалить Line D2. Но это привело к сбою компиляции:

new.cc: В destructor 'virtual B:: ~ B(): new.cc:9:8: ошибка: нет подходящий 'оператор delete для' B struct B: A          ^ new.cc: В глобальной области: new.cc:18:31: note: синтезированный метод 'virtual B:: ~ B() сначала требуется здесь B * doit1() {return новый (p) B; }

Небольшое исследование определило, что проблема - виртуальный деструктор B. Он должен вызывать не размещение B::operator delete, потому что кто-то может попробовать delete a B через A *. Благодаря скрытию имени строка D1 делает недоступным по умолчанию размещение operator delete недоступным.

Мой вопрос: как лучше всего справиться с этим? Одно очевидное решение:

static void operator delete(void *m) { std::terminate(); } // Line D2

Но это кажется неправильным... Я имею в виду, кем я должен настаивать на том, что вы должны выделить эти вещи из пула?

Еще одно очевидное решение (и то, что я сейчас использую):

static void operator delete(void *m) { ::operator delete(m); } // Line D2

Но это также кажется неправильным, потому что, как я знаю, я вызываю функцию правильного удаления?

То, что я действительно хочу, я думаю, это using A::operator delete;, но это не скомпилируется ( "никакие члены не сопоставляют" оператор A:: "delete в" struct A ").

Связанные, но разные вопросы:

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

Clang жалуется "не может переопределить удаленную функцию" в то время как никакая функция не удаляется

[Обновить, чтобы развернуть бит]

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

Также, чтобы быть понятным по желаемому поведению... Нормальный пример использования выглядит так:

Pool p;
B *b = new (p) B;
...
b->~B();
// worry about the pool later

То есть, как и большинство применений размещения new, вы вызываете деструктор напрямую. Или вызовите вспомогательную функцию, чтобы сделать это для вас.

Я не ожидал, что следующее будет работать; на самом деле, я бы счел это ошибкой:

Pool p;
A *b_upcast = new (p) B;
delete b_upcast;

Обнаружение и отказ при таком ошибочном использовании будет прекрасным, но только если это можно сделать без добавления каких-либо накладных расходов в не ошибочные случаи. (Я подозреваю, что это невозможно.)

Наконец, я ожидаю, что это сработает:

A *b_upcast = new B;
delete b_upcast;

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

Мое текущее решение работает в основном, но я обеспокоен тем, что прямой вызов ::operator delete не всегда прав.

Если вы думаете, что у вас есть хороший аргумент, что мои ожидания в отношении того, что должно или не должно работать, являются неправильными, я тоже хотел бы это услышать.

4b9b3361

Ответ 1

Интересная проблема. Если я правильно понял, что вы хотите сделать, это выбрать правильный оператор удаления в зависимости от того, было ли оно распределено через пул или нет.

Вы можете сохранить дополнительную информацию об этом в начале выделенного блока из пула.

Так как B не может быть выделен без пула, вам просто нужно переслать его на место размещения внутри обычного оператора delete (void *), используя бит дополнительной информации о пуле.

Оператор new сохранит эту часть в начале выделенного блока.

UPDATE: Спасибо за разъяснения. Такой же трюк все еще работает с некоторыми незначительными изменениями. Обновленный код ниже. Если это еще не то, что вы хотите сделать, пожалуйста, предоставьте некоторые положительные и отрицательные тестовые примеры, чтобы определить, что должно и что не должно работать.

struct Pool
{
    void* alloc(size_t s) {
        // do the magic... 
        // e.g. 
        //    return buf;
        return buf;
    }
    void dealloc(void* m) {
        // more magic ... 
    }
private:

    char buf[256];
};
struct PoolDescriptor {
    Pool* pool;
};


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

struct B : A
{
    static void *operator new(std::size_t s){
        auto desc = static_cast<PoolDescriptor*>(::operator new(sizeof(PoolDescriptor) + s));
        desc->pool = nullptr;
        return desc + 1;
    }

    static void *operator new(std::size_t s, Pool &p){
        auto desc = static_cast<PoolDescriptor*>(p.alloc(sizeof(PoolDescriptor) + s));
        desc->pool = &p;
        return desc + 1;
    }
    static void operator delete(void *m, Pool &p) {
        auto desc = static_cast<PoolDescriptor*>(m) - 1;
        p.dealloc(desc);
    }
    static void operator delete(void *m) {
        auto desc = static_cast<PoolDescriptor*>(m) - 1;
        if (desc->pool != nullptr) {
            throw std::bad_alloc();
        }
        else {
            ::operator delete (desc);
        } // Line D2
    }
};


Pool p;
void shouldFail() { 
    A* a = new(p)B;
    delete a;
}
void shouldWork() { 
    A* a = new B;
    delete a;
}

int main()
{
    shouldWork();
    shouldFail();
    return 0;
}

Ответ 2

Очень сложно понять, что вы собираетесь достичь с помощью этого кода, поскольку вы лишили его важных бит.

Знаете ли вы, что static void operator delete(void *m, Pool &p) { } вызывается только в том случае, если конструктор B выбрасывает исключение?

15) Если определено, вызывается обычным размещением одного объекта new выражение с соответствующей сигнатурой, если конструктор объекта выдает исключение. Если определена определенная для класса версия (25), она (9). Если не предусмотрено ни (25), ни (15) пользователем не вызывается функция освобождения.

Это означает, что в текущем примере этот оператор delete (D1) никогда не будет вызываться.

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

Если вам действительно нужен базовый класс A и добавлен виртуальный деструктор, чтобы отключить предупреждения, вы можете сделать деструктор защищенным в вместо того, чтобы сделать его виртуальным. Как это -

struct A
{
protected:
  ~A() { }
};

struct B final : public A
{
  ~B() = default;

  static void *operator new(std::size_t s, Pool &p) { return &p.buf[0]; }
  static void operator delete(void *m, Pool &p) {} // Line D1

  static void operator delete(void *m) {} // Line D2

};