Как работает удаление размещения C++ внутри (среда выполнения C++)? Как преодолеть его ограничение?

C++ асимметричен по размещению new и размещению delete. Нам разрешено перегружать размещение new почти произвольным образом. Однако функции удаления места размещения только вызываются из новых выражений места размещения. В частности, они вызываются, если конструктор объекта выдает исключение. Нет никакого способа вызвать удаление размещения для кода приложения вообще.

У меня есть следующие путаницы и вопросы, которые необходимо уточнить:

1) Почему компилятор С++ не может просто отклонить сигнатуру нового метода размещения, если не определен аналог удаления размещения? Это может помочь убить возможности утечки памяти в этом контексте.

2) Если у меня есть несколько пулов памяти (управляемых кодом приложения), и я хочу, чтобы новое размещение выделяло память из разных пулов, просто нет возможности поддерживать это из-за того, что нет способа узнать, какой пул памяти указатель пришел из оператора удаления? (оператор delete имеет только информацию о void*). Есть ли способ сделать это на С++?

struct Node {
    void* operator new(size_t size, Strategy s) {
        // Depend on different strategy, allocate memory
        // from different Memory pool
    }

    void operator delete(void* memory, Strategy s) {
        // Return the memory to different Memory pool
        // depends on the Strategy
        // However this delete will only be invoked by
        // c++ runtime if Node constructor throws.
    }

    void operator delete(void* memory, size_t s) {
        // This delete doesn't have any clue about the
        // the strategy. Hence, it can't decide which
        // Memory pool to return to.
    }
}

3) В контексте нового размещения среда выполнения C++ вызовет удаление размещения с тем же аргументом. Как среда выполнения C++ выполняет это?


person Oliver Young    schedule 30.05.2019    source источник
comment
Можете ли вы использовать умный указатель?   -  person curiousguy    schedule 31.05.2019
comment
Ваш комментарий не имеет отношения к делу.   -  person Oliver Young    schedule 31.05.2019
comment
Если вы делаете T* p = new (memory) T();, разве вы не должны делать p->~T();? Что такое место размещения delete?   -  person jamesdlin    schedule 31.05.2019
comment
Вам не нужно размещение new: он просто вызывает деструктор, и вы можете просто сделать это напрямую. Поэтому нет причин его называть. Подождите, вы переопределяете новое поведение места размещения? Это вообще разрешено? Это определенно не очень хорошая идея...   -  person Mooing Duck    schedule 31.05.2019
comment
@jamesdlin: я думаю, что он переопределяет новое размещение, чтобы иметь дополнительное поведение, и пытается переопределить удаление размещения, чтобы убрать это дополнительное поведение. Но форма удаления места размещения вызывается только из нового места размещения и в противном случае не может быть вызвана. Я никогда раньше не слышал об этом, потому что я никогда раньше не слышал, чтобы кто-то переопределял новое размещение.   -  person Mooing Duck    schedule 31.05.2019
comment
@MooingDuck Функции распределения, вызываемые new, являются заменяемыми, поэтому пользователь может управлять выделением (и/или отлаживать его)   -  person M.M    schedule 31.05.2019
comment
@MM: Да, я имею в виду функции распределения мест размещения и функции освобождения мест размещения.   -  person Oliver Young    schedule 31.05.2019
comment
Дизайн оператора new/delete несколько нарушен в C++. То, что мы не можем передавать параметры в delete, на мой взгляд, плохо. Единственное решение, которое я знаю, это полностью избегать использования оператора new/delete (кроме размещения new) и запускать свою собственную систему.   -  person geza    schedule 31.05.2019
comment
@geza Как нам избежать оператора new, если мы хотим контролировать управление памятью? Если мы создадим новый объект, компилятор неизбежно вызовет для нас оператор new. Вы хотели сказать, что не перегружаете оператор new/delete (за исключением использования нового размещения по умолчанию?)   -  person Oliver Young    schedule 31.05.2019
comment
@OliverYoung: Общая идея заключается в том, что вместо Foo *f = new(myAllocator) Foo(a, b, c); используйте Foo *f = myAllocator->allocate<Foo>(a, b, c);, а вместо delete используйте myAllocator->free(f);.   -  person geza    schedule 01.06.2019
comment
@geza Я вижу. Я предполагаю, что myAllocator-›allocate‹Foo› и myAllocator-›free‹Foo› будут вызывать конструктор и деструктор внутри себя?   -  person Oliver Young    schedule 06.06.2019
comment
@OliverYoung: Да. Конструктор вызывается путем размещения new.   -  person geza    schedule 06.06.2019


Ответы (2)


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

void* operator new  ( std::size_t count, user-defined-args... );
void* operator new[]( std::size_t count, user-defined-args... );

и определяемые пользователем функции освобождения места размещения:

void operator delete  ( void* ptr, args... );
void operator delete[]( void* ptr, args... );

Поведение этих функций:

  • operator new: если определено, вызывается новым выражением пользовательского размещения одиночного объекта с соответствующей сигнатурой. Если определена специфичная для класса версия, она вызывается вместо этой. Если пользователь не предоставил ни того, ни другого, новое выражение размещения имеет неправильный формат.
  • operator new[]: То же самое, но для формы массива.
  • operator delete: если определено, вызывается выражением new для пользовательского размещения одного объекта с соответствующей сигнатурой, если конструктор объекта выдает исключение. Если определена специфичная для класса версия, она вызывается вместо этой. Если ни один из них не указан пользователем, функция освобождения не вызывается.
  • operator delete[]: То же самое, но для формы массива.

Чтобы ответить на ваши вопросы:

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

  2. Вам придется включить механизм, с помощью которого значение удаляемого указателя void * можно использовать для определения того, в каком пуле памяти он находится. Это должно быть возможно, потому что ваши разные пулы не могут возвращать одно и то же значение одновременно. Наивный метод может заключаться в том, чтобы каждый пул управлял непересекающимся диапазоном адресов и проверял указатель на соответствие диапазону адресов каждого пула. Или, возможно, вы могли бы хранить метаданные в памяти до указанного места, например. версия new будет выделять N+16 байтов, хранить метаданные в первых 16 и возвращать пользователю указатель на 16-й байт блока). Или вы можете сохранить структуру данных, связывающую каждый активный указатель с метаданными.

  3. Оценка нового выражения для размещения объекта типа класса будет выглядеть примерно так:

    • Call operator new, passing the arguments
    • Предполагая, что это удается, вызовите конструктор класса (если он выделяется типом класса).
    • Если конструктор класса вызывает исключение и существует соответствие operator delete, вызовите эту функцию, передав аргументы.
    • Теперь вычисление нового-выражения завершено, и управление возвращается либо к коду, содержавшему новое-выражение, либо к механизму обработки исключений.
person M.M    schedule 31.05.2019
comment
Спасибо за подробный ответ. 1) Не могли бы вы указать мне практический пример, когда функция освобождения не требуется? Разве не намного безопаснее, если компилятор может выдать ошибку во время компиляции, чтобы предотвратить потенциальную утечку памяти? 2) Хранение метаданных более практично, что используется во многих проектах. Однако это основано на предположении, что все разные пулы памяти управляются одной и той же библиотекой управления. Это может быть не так. Допустим, у меня есть две сторонние библиотеки управления памятью A и B. Передача адреса памяти, выделенного B, в библиотеку A может привести к повреждению памяти. - person Oliver Young; 31.05.2019
comment
3) Я понимаю механику. Но я пытаюсь понять, как C++ выполняет третий пункт: если конструктор класса выдает исключение и существует соответствующий оператор удаления, вызовите эту функцию, передав аргументы. Как он проверяет, существует ли соответствующий оператор удаления? Я предполагаю, что компилятор С++ проверяет это во время компиляции? (В любом случае я не могу придумать, чтобы проверить это во время выполнения). Если это так, то возвращаюсь к моему вопросу номер один. Я думаю, что может быть лучше ошибиться во время компиляции, если соответствующий оператор удаления не существует... - person Oliver Young; 31.05.2019
comment
@OliverYoung (1) некоторая задача может только когда-либо выделяться и никогда не выходить, или запускаться в ОС, которая освобождает память процесса при выходе. (2) вы можете включить идентификатор библиотеки в свои метаданные, (3) компилятор/компоновщик должен знать, существуют ли эти функции или нет. С++ не держит вас за руку, и это сложная тема, если вы пишете распределитель памяти, вы должны быть в состоянии предоставить совпадающие подписи и протестировать свой код. - person M.M; 31.05.2019
comment
(1) Для такого использования дизайн C++ может попросить пользователя предоставить оператор удаления без операции. На самом деле это то, что делает стандартное удаление размещения C++, не так ли? (void* оператор delete(void*, void*) ). И ошибка во время компиляции, если не существует соответствующего оператора удаления. - person Oliver Young; 31.05.2019
comment
Я пытаюсь понять конструктивное обоснование этого. С одной стороны, C++ просит пользователя всегда вызывать оператор удаления по умолчанию, когда исключение не выдается, с предположением, что пользователь должен полагаться на метаданные, хранящиеся рядом с ячейкой памяти, чтобы выяснить, что делать. С другой стороны, C++ просит пользователя предоставить перегруженный оператор удаления, чтобы предотвратить утечку памяти при возникновении исключения. Было бы неплохо, если бы C++ мог унифицировать механизм освобождения памяти? - person Oliver Young; 31.05.2019
comment
@OliverYoung operator delete(void*, void*) (элемент 9 в cppreference) всегда существует, компилятор предоставляет его, и пользователь может при желании заменить его. Это отличается от определяемого пользователем размещения (де-)распределения, где ничего не предоставляется, поскольку вы создаете подпись, которую хотите для версии распределения. Я не понимаю, что вы подразумеваете под унификацией механизма освобождения. Если вы предлагаете, чтобы компилятор запоминал, какие аргументы были предоставлены new для каждого отдельного указателя, это было бы огромным снижением производительности для пользователей, которым это не требуется. - person M.M; 31.05.2019
comment
(1) Я знаю, что всегда существует оператор удаления (void*, void*). Я думаю, что это способ С++ для обработки особого случая (т.е. без операции), когда обычный оператор удаления не может справиться. Как вы указали, может быть задача, не требующая освобождения. В этом случае C++ может потребовать от пользователей предоставления аналогичного определяемого пользователем оператора удаления без операции. C++ может выбрать отклонение/выдачу ошибки, когда аналог оператора удаления не существует, вместо молчаливой утечки памяти при возникновении исключения. Я чувствую, что это гораздо более безопасный подход. - person Oliver Young; 31.05.2019
comment
(2) Под унификацией я подразумеваю, что С++ может последовательно выбирать одну из следующих двух политик освобождения, независимо от того, выброшено исключение или нет. (a) Перегрузить свою логику освобождения на основе метаданных, хранящихся рядом с адресом памяти. или (b) перегруженная логика освобождения, основанная на перегрузке оператора удаления. Попросите пользователя полагаться на политику (а), когда исключение не выбрасывается из конструктора, и в то же время использовать политику времени выполнения C++ (б), когда выдается исключение. Это кажется мне немного неинтуитивным. - person Oliver Young; 31.05.2019
comment
Для (а) выбор остается за пользователем. Вам не нужно использовать метаданные, хранящиеся рядом с адресом. Если бы реализация требовала чего-то подобного, это было бы наказанием для тех, кому это не нужно. - person M.M; 31.05.2019

Посмотрев это: вы должны предоставить Node::destroy, который избавляется от узла и возвращает его в вашу локальную кучу.

Если бы они были общими new с for void*, у нас было бы следующее уничтожение:

template<class T> void destroy (Strategy s, T* ptr)
{
    ptr->~T();
    // return to custom heap
 }

Вы можете использовать причудливые трюки, чтобы избавиться от стратегии, например, проверить, в какой куче находится ptr, или сохранить его с отрицательным смещением в new.

Бесполезный исторический ответ:

Это было давно, но я помню синтаксис

delete (strategy) pointer;

Похоже, это ерунда конкретного поставщика из BC4.5.

person Joshua    schedule 31.05.2019
comment
Невозможно запретить пользователю писать delete ptr; вместо вызова destroy, поэтому, возможно, нужно реализовать предполагаемое поведение для delete ptr; с помощью так называемых причудливых трюков и предложить эту опцию в качестве оптимизации, когда вызывающая сторона знает стратегию. . - person M.M; 31.05.2019
comment
@M.M: Я только когда-либо делал new (size_t, int) без типа, так что это было мне недоступно. Прямой звонок delete был ожидаемым несчастным случаем. - person Joshua; 31.05.2019