Как работает удаление размещения 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++ выполняет это?

Можете ли вы использовать «умный указатель»?

curiousguy 31.05.2019 01:45

Ваш комментарий не имеет отношения к делу.

Oliver Young 31.05.2019 01:49

Если вы делаете T* p = new (memory) T();, разве вы не должны делать p->~T();? Что такое «размещение delete»?

jamesdlin 31.05.2019 01:49

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

Mooing Duck 31.05.2019 01:49

@jamesdlin: я думаю, что он переопределяет новое размещение, чтобы иметь дополнительное поведение, и пытается переопределить удаление размещения, чтобы убрать это дополнительное поведение. Но форма удаления места размещения вызывается только из нового места размещения и в противном случае не может быть вызвана. Я никогда раньше не слышал об этом, потому что я никогда раньше не слышал, чтобы кто-то переопределял новое размещение.

Mooing Duck 31.05.2019 01:51

@MooingDuck Функции выделения, которые вызываются new, являются заменяемыми, поэтому пользователь может контролировать выделение (и/или отлаживать его)

M.M 31.05.2019 01:51

@MM: Да, я имею в виду функции распределения мест размещения и функции освобождения мест размещения.

Oliver Young 31.05.2019 02:10

Дизайн оператора new/delete несколько нарушен в C++. То, что мы не можем передавать параметры в delete, на мой взгляд, плохо. Единственное решение, которое я знаю, это полностью избегать использования оператора new/delete (кроме размещения new) и запускать свою собственную систему.

geza 31.05.2019 14:05

@geza Как нам избежать оператора new, если мы хотим контролировать управление памятью? Если мы создадим новый объект, компилятор неизбежно вызовет для нас оператор new. Вы хотели сказать, что не перегружаете оператор new/delete (за исключением использования нового размещения по умолчанию?)

Oliver Young 31.05.2019 20:52

@OliverYoung: Общая идея заключается в том, что вместо Foo *f = new(myAllocator) Foo(a, b, c); используйте Foo *f = myAllocator->allocate<Foo>(a, b, c);, а вместо delete используйте myAllocator->free(f);.

geza 01.06.2019 10:06

@geza Я вижу. Я предполагаю, что myAllocator->allocate<Foo> и myAllocator->free<Foo> будут вызывать конструктор и деструктор внутри?

Oliver Young 06.06.2019 00:35

@OliverYoung: Да. Конструктор вызывается путем размещения new.

geza 06.06.2019 08:46
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
12
408
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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. Оценка нового выражения для размещения объекта типа класса будет выглядеть примерно так:

    • Вызов operator new, передав аргументы
    • Предполагая, что это удается, вызовите конструктор класса (если он выделяется типом класса).
    • Если конструктор класса выдает исключение и существует соответствие operator delete, вызовите эту функцию, передав аргументы.
    • Теперь вычисление нового-выражения завершено, и управление возвращается либо к коду, содержавшему новое-выражение, либо к механизму обработки исключений.

Спасибо за подробный ответ. 1) Не могли бы вы указать мне практический пример, когда функция освобождения не требуется? Разве не намного безопаснее, если компилятор может выдать ошибку во время компиляции, чтобы предотвратить потенциальную утечку памяти? 2) Хранение метаданных более практично, что используется во многих проектах. Однако это основано на предположении, что все разные пулы памяти управляются одной и той же библиотекой управления. Это может быть не так. Допустим, у меня есть две сторонние библиотеки управления памятью A и B. Передача адреса памяти, выделенного B, в библиотеку A может привести к повреждению памяти.

Oliver Young 31.05.2019 05:51

3) Я понимаю механику. Но я пытаюсь понять, как C++ выполняет третий пункт: «Если конструктор класса выдает исключение и существует соответствующий оператор удаления, вызовите эту функцию, передав аргументы». Как он проверяет, существует ли соответствующий оператор удаления? Я предполагаю, что компилятор С++ проверяет это во время компиляции? (В любом случае я не могу придумать, чтобы проверить это во время выполнения). Если это так, то возвращаюсь к моему вопросу номер один. Я думаю, что может быть лучше ошибиться во время компиляции, если соответствующий оператор удаления не существует...

Oliver Young 31.05.2019 05:57

@OliverYoung (1) некоторая задача может только когда-либо выделяться и никогда не выходить, или запускаться в ОС, которая освобождает память процесса при выходе. (2) вы можете включить идентификатор библиотеки в свои метаданные, (3) компилятор/компоновщик должен знать, существуют ли эти функции или нет. С++ не держит вас за руку, и это сложная тема, если вы пишете распределитель памяти, вы должны быть в состоянии предоставить совпадающие подписи и протестировать свой код.

M.M 31.05.2019 06:29

(1) Для такого использования дизайн C++ может попросить пользователя предоставить оператор удаления без операции. На самом деле это то, что делает стандартное удаление размещения C++, не так ли? (void* оператор delete(void*, void*) ). И ошибка во время компиляции, если не существует соответствующего оператора удаления.

Oliver Young 31.05.2019 06:40

Я пытаюсь понять конструктивное обоснование этого. С одной стороны, C++ просит пользователя всегда вызывать оператор удаления по умолчанию, когда не выдается исключение, с предположением, что пользователь должен полагаться на метаданные, хранящиеся рядом с ячейкой памяти, чтобы выяснить, что делать. С другой стороны, C++ просит пользователя предоставить перегруженный оператор удаления, чтобы предотвратить утечку памяти при возникновении исключения. Было бы неплохо, если бы C++ мог унифицировать механизм освобождения памяти?

Oliver Young 31.05.2019 06:42

@OliverYoung operator delete(void*, void*) (элемент 9 в cppreference) всегда существует, компилятор предоставляет его, и пользователь может при желании заменить его. Это отличается от определяемого пользователем размещения (де-)распределения, где ничего не предоставляется, поскольку вы создаете подпись, которую хотите для версии распределения. Я не понимаю, что вы подразумеваете под «унифицировать механизм освобождения». Если вы предлагаете, чтобы компилятор «запоминал», какие аргументы были предоставлены new для каждого отдельного указателя, это было бы огромным снижением производительности для пользователей, которым это не требуется.

M.M 31.05.2019 07:39

(1) Я знаю, что всегда существует оператор удаления (void*, void*). Я думаю, что это способ С++ для обработки особого случая (например, «без операции»), когда обычный оператор удаления не может справиться. Как вы указали, может быть задача, не требующая освобождения. В этом случае C++ может потребовать, чтобы пользователи предоставили аналогичный определяемый пользователем оператор удаления без операции. C++ может выбрать отклонение/выдачу ошибки, когда аналог оператора удаления не существует, вместо молчаливой утечки памяти при возникновении исключения. Я чувствую, что это гораздо более безопасный подход.

Oliver Young 31.05.2019 07:52

(2) Под унификацией я подразумеваю, что С++ может последовательно выбирать одну из следующих двух политик освобождения, независимо от того, выброшено исключение или нет. (a) Перегрузить свою логику освобождения на основе метаданных, хранящихся рядом с адресом памяти. или (b) перегруженная логика освобождения, основанная на перегрузке оператора удаления. Попросите пользователя полагаться на политику (а), когда исключение не выбрасывается из конструктора, и в то же время использовать политику среды выполнения C++ (б), когда выдается исключение. Это кажется мне немного неинтуитивным.

Oliver Young 31.05.2019 07:58

Для (а) выбор остается за пользователем. Вам не нужно использовать метаданные, хранящиеся рядом с адресом. Если бы реализация требовала чего-то подобного, это было бы наказанием для тех, кому это не нужно.

M.M 31.05.2019 08:09

Посмотрев это: вы должны предоставить 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.

Невозможно запретить пользователю писать delete ptr; вместо вызова destroy, поэтому, возможно, нужно реализовать предполагаемое поведение для delete ptr; с помощью так называемых «причудливых трюков» и предложить этот вариант в качестве оптимизации, когда стратегия известна звонящий.

M.M 31.05.2019 03:09

@M.M: Я когда-либо делал new (size_t, int) только без типа, так что это было мне недоступно. Прямой звонок delete был ожидаемым несчастным случаем.

Joshua 31.05.2019 03:13

Другие вопросы по теме