Преднамеренная утечка памяти в std :: vector - PullRequest
1 голос
/ 16 октября 2019

Мне нужно найти способ преднамеренной утечки (владения) внутреннего указателя std::vector, чтобы его время жизни превышало срок службы исходного контейнера и чтобы впоследствии его можно было удалить вручную.

Почему? Я работаю над сетевым приложением, использующим библиотеку C ENet, которая должна отправлять большое количество пакетов за короткое время.

Я создаю сетевые сообщения, записывая данные в std::vector<unsigned char>.

Затем, чтобы создать «пакет», я использую функцию enet_packet_create, которая принимает указатель на байтовый массив и его размер. В обычном режиме работы функция просто динамически дублирует данный массив в куче, но есть также опция «без выделения», которая принимает только указатель и размер, оставляя удаление создателю с помощью функции обратного вызова, и это именно то, чтоЯ пытаюсь добиться - данные уже есть в векторе, готовом к использованию, поэтому нет необходимости копировать их снова, поскольку это может быть дорогостоящим.

Ответы [ 4 ]

2 голосов
/ 16 октября 2019

Вам не нужно ничего пропускать. Просто используйте поле userData структуры ENetPacket, чтобы сохранить подлежащее удалению std::vector, и просто удалите его в обратном вызове:

void myCallback(ENetPacket *pkt) {
    std::vector<uint8_t> *data=(std::vector<uint8_t> *)pkt->userData;
    delete data;
}

void sendData() {
    //Create the vector in heap, so it is not destroyed after returning from this function, effectively extending its life until the callback is called.
    std::vector<uint8_t> *data=new std::vector<uint8_t>;
    //Fill data here
    ENetPacket *pkt=enet_packet_create(data.data(), data.size(), ENET_PACKET_FLAG_NO_ALLOCATE);
    pkt->userData=(void*)data;
    pkt->freeCallback=myCallback;

}

userData void указатель - это обычная стратегия для хранения непрозрачных пользовательских данных и использования их в обратных вызовах, поэтому пользователь библиотеки может получить контекст, в котором был вызван обратный вызов.

Это может быть что угодно (void*), из структуры держателей состояний для выполнения сложной логики после обратного вызова, или просто указатель данных, который должен быть освобожден, как ваш случай.


Из ваших комментариев вы говорите, что выне хочу динамически выделять vector.

Просто помните, что любые данные внутри вектора были динамически распределены (если не был использован пользовательский распределитель), и структура ENetPacket также была динамически распределена(переданный флаг просто указывает, что не следует выделять data, а не структуру)


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

Функция enet_packet_create создаст буфер данных, и вы можете просто заполнить данные непосредственно в буфере пакетов, не требуя другого буфера, а затем скопировать его в пакет.

2 голосов
/ 16 октября 2019

Этот подход невозможен, даже если vector<T> предоставил интерфейс, позволяющий вам скрыться с его памятью. Давайте разберемся почему.

Ваша проблема существует, потому что сайт, на котором вы собираетесь освободить память, не содержит произвольных данных. Дается только указатель на память, которая будет освобождена. Если бы это было не так, то вы просто передали бы указатель на vector<T> в это место или иным образом переправили бы сам объект vector<T>.

Для того, чтобы скрыться с vector<T> памяти и успешно освободить ее, вам придется играть по правилам vector<T>. Что означает:

  1. Вы должны уважать различие размера / емкости. Не вся память, выделенная для vector<T>, на самом деле содержит живые T с. Таким образом, вы должны знать, сколько живых T s существует в этой памяти, чтобы вы могли правильно вызывать их деструкторы (об этом мы поговорим позже).

    Теперь точно, для очень специфический случай unsigned char, вызов деструкторов не имеет значения, поскольку они тривиальны. Но интерфейс vector<T> должен быть единообразным;если вы можете скрыться с памятью vector<unsigned char>, то вы должны быть в состоянии скрыться с любым vector<T> таким же образом. Поэтому любой скрывающийся интерфейс должен обеспечивать не только указатель на данные, но также размер и емкость, чтобы вы могли должным образом уничтожить элементы контейнера.

  2. Вы должны соблюдать Allocator. Помните: шаблон vector<T, Allocator>, где Allocator - это тип, который выполняет выделение / освобождение памяти, а также создание / уничтожение фактических T s в vector. А поскольку вам разрешено предоставлять конкретные объекты конкретного экземпляра Allocator, любой скрывающийся интерфейс должен хранить этот конкретный объект Allocator (или копировать / перемещать его), чтобы выделение могло быть освобождено.

    Опять же, конкретный случай vector<unsigned char> не имеет значения, потому что распределитель по умолчанию std::allocator просто использует ::operator new/delete для выделения / освобождения памяти и прямые вызовы размещения-нового / деструктора для создания / уничтожения T s. Но опять же, общий скрывающийся интерфейс должен работать с любым T и любым Allocator. Поэтому он должен учитывать все это.

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

Короче говоря, побег из памяти vector<T, Allocator> означает создание vector<T, Allocator>.

, что вы не можете сделать, так какуказано выше. Вы попали в противоречивую ситуацию.

Есть два решения:

  1. Измените свой код, чтобы вы могли переправить в vector<T> в это место. Это можно сделать с помощью некоторой глобальной карты / области видимости / etc из указателя на данные в vector<unsigned char>*. Или какой-то другой механизм. Вы должны это выяснить, потому что это зависит от конкретных аспектов системы, которые вы не представили (это определение XY Problem ).

  2. Прекратить использование vector<unsigned char>. Вместо этого просто выделите кучу массив unsigned char, который вы можете просто уничтожить.

1 голос
/ 16 октября 2019

Мне нужно найти способ преднамеренной утечки внутреннего указателя std::vector

Единственный способ утечки внутреннего буфера std::vector - это утечка самого вектора. Пример:

std::vector<T>* ptr = new std::vector<T>;
ptr = nullptr; // memory leaked succesfully

Но утечка памяти вообще не очень хорошая идея.

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

В этом случае единственное решение - убедиться, что время жизни std::vector больше, чем использование буфера. Вектор всегда освобождает принадлежащий ему буфер при уничтожении, и нет никакого способа извлечь из него владельца, кроме как в другой вектор.

Один из способов добиться этого заключается в следующем:

// stored somewhere with guaranteed longer lifetime than any packet
std::unordered_map<unsigned char*, std::vector<unsigned char>> storage;

void foo()
{
    std::vector<unsigned char> vec;
    // fill vec here
    unsigned char* ptr = vec.data();
    storage[ptr] = std::move(vec);
    auto destroy_callback = [](unsigned char* ptr) {
        storage.erase(ptr);
    }
    // pass ptr and destroy_callback into some async API
}

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

Пример адаптированной формы этот ответ (теперь, когда этот вопрос перешел от утечки к передаче права собственности, это близко к дубликату),Есть также альтернативное предложение в другом ответе на тот же вопрос, который использует пользовательский распределитель, который «крадет» собственность

0 голосов
/ 16 октября 2019

Следующее не является ответом! Это еще одна попытка убедить вас переосмыслить свой подход, но это слишком долго для комментариев. (Сказав это, я должен сказать, что я люблю этот тип хаков, когда это просто для удовольствия, но я ненавижу их еще сильнее, когда они переходят в рабочий код.)

Из OP, мотивация использовать «no alloc» опция позволяет избежать выделения памяти и копирования байтов внутри enet_packet_create. Это вызывает у меня вопрос, почему использовать vector?

Если вы создаете vector, но не фиксируете его емкость (с reserve или resize) с самого начала и вместо этого позволяетеувеличивая его по мере добавления элементов, каждый раз при увеличении емкости vector будет выделять память и копировать байты, чего вы точно хотите избежать.

Возможно, вы знаете с самого начала, каков окончательный размерvector будет. В этом случае вы можете избежать всех копий и выделения памяти (кроме одного), зарезервировав этот размер с самого начала. В таком случае, почему бы просто не использовать new[] и delete[], как предложил Квентин? Тебе не нужно будет красть память, потому что она будет твоей. Более того, вы можете создать unique_ptr<unsigned char[]> (рассмотрим make_unique<unsigned char[]>), использовать его метод release непосредственно перед вызовом enet_packet_create, чтобы «украсть» память, а затем вызвать delete[], чтобы освободить память.

...