получение памяти от std::vector

Я использую внешнюю библиотеку, которая работает с большими объемами данных. Данные передаются необработанным указателем плюс длина. Библиотека не претендует на владение указателем, но вызывает предоставленную функцию обратного вызова (с теми же двумя аргументами), когда это делается с данными.

Данные подготавливаются удобно с помощью std::vector<T>, и я бы не хотел отказываться от этого удобства. О копировании данных не может быть и речи. Таким образом, мне нужен способ «захватить» буфер памяти, принадлежащий std::vector<T>, и (позже) освободить его в обратном вызове.

Мое текущее решение выглядит следующим образом:

std::vector<T> input = prepare_input();
T * data = input.data();
size_t size = input.size();
// move the vector to "raw" storage, to prevent deallocation
alignas(std::vector<T>) char temp[sizeof(std::vector<T>)];
new (temp) std::vector<T>(std::move(input));
// invoke the library
lib::startProcesing(data, size);

и в функции обратного вызова:

void callback(T * data, size_t size) {
    std::allocator<T>().deallocate(data, size);
}

Это решение работает, потому что функция deallocate стандартного распределителя игнорирует свой второй аргумент (количество элементов) и просто вызывает ::operator delete(data). В противном случае могли бы произойти плохие вещи, так как size входного вектора может быть немного меньше, чем его capacity.

Мой вопрос: существует ли надежный (по стандарту С++) способ захвата буфера std::vector и освобождения его "вручную" через какое-то время?


person Grzegorz Herman    schedule 26.11.2014    source источник
comment
Вам нужно взять на себя весь вектор.   -  person T.C.    schedule 27.11.2014
comment
Было бы неплохо, если бы у vector была функция detach... но это не так   -  person M.M    schedule 27.11.2014
comment
@T.C.: но мне негде его хранить - создание ввода и освобождение происходят в двух разных частях программы   -  person Grzegorz Herman    schedule 27.11.2014
comment
Я не понимаю необходимости выровненного хранилища. Почему бы не просто unique_ptr<vector<T>> temp(new vector<T>(move(input)));? Кроме того, ваше решение работает только в том случае, если T является тривиально разрушаемым типом, иначе вам нужно будет вызывать allocator<T>::destroy для каждого элемента. Чтобы ответить на ваш вопрос, нет простого способа получить память от vector, возможно, вы сможете что-то сделать, используя собственный распределитель, но я бы просто придерживался текущего решения.   -  person Praetorian    schedule 27.11.2014
comment
Возможно, вместо подготовки ввода в vector вы могли бы создать свой собственный векторный класс, который имеет функцию detach.   -  person M.M    schedule 27.11.2014
comment
Вздох — еще один случай, когда библиотеки используют плохие сигнатуры обратного вызова. Если бы сигнатура обратного вызова была void (*callback)(T * data, size_t size, void * user_data) и startProcessing(T* data, size_t size, void * userdata), у вас был бы простой путь к решению.   -  person Michael Anderson    schedule 27.11.2014
comment
(а) ваше существующее решение кажется мне правильным, и (б) есть ли какая-либо причина, по которой вам нужно освободить место в обратном вызове вместо того, чтобы просто использовать исходный вектор и очистить его после завершения обработки?   -  person M.M    schedule 27.11.2014
comment
@Praetorian: я использовал хранилище стека только для того, чтобы избежать манипуляций с кучей (моя привычка), конечно, его нужно было правильно выровнять, иначе мог произойти сбой конструктора перемещения; и да, вы правы, я забыл упомянуть деструкторы (хотя, поскольку я знаю количество объектов, они не представляют проблемы)   -  person Grzegorz Herman    schedule 27.11.2014
comment
@MichaelAnderson это, вероятно, можно было бы устроить в любом случае (например, статическая переменная)   -  person M.M    schedule 27.11.2014
comment
@MattMcNabb: (a) я бы предпочел избежать поломки пары обновлений реализации STL; (b) это происходит в другом потоке в неуказанный момент времени (всякий раз, когда библиотека завершает работу с этим)   -  person Grzegorz Herman    schedule 27.11.2014
comment
@GrzegorzHerman Вы позволяете массиву temp выйти за рамки? Даже если это сработает на практике, наверняка это неопределенное поведение? (Я спрашиваю, я не знаю ответа)   -  person Praetorian    schedule 27.11.2014
comment
@GrzegorzHerman AFAICS не полагается на какое-либо нестандартное поведение.   -  person M.M    schedule 27.11.2014
comment
В другой ветке? Теперь это банка с червями.   -  person M.M    schedule 27.11.2014
comment
@MattMcNabb: почему temp выходит за рамки неопределенного поведения (вместо того, чтобы просто не вызывался деструктор того, что было там помещено)? В документации std::allocator<T>::deallocate указано, что вторым аргументом должен быть именно тот, который был передан в allocate -- безусловно, нарушение спецификации является нестандартным.   -  person Grzegorz Herman    schedule 27.11.2014
comment
Хорошо, достаточно честно. Возможно, вы можете рассмотреть возможность того, чтобы вектор был статическим, а обратный вызов очищал вектор (в этом случае новое размещение не требуется)   -  person M.M    schedule 27.11.2014
comment
@MattMcNabb: библиотека обработки требует многопоточности (я могу начать обрабатывать, возможно, несколько пакетов данных параллельно и тем временем сделать что-то полезное) - но здесь это не имеет значения   -  person Grzegorz Herman    schedule 27.11.2014
comment
@MattMcNabb: статический в чем? помните, их может быть много одновременно...   -  person Grzegorz Herman    schedule 27.11.2014
comment
@MattMcNabb Эта сигнатура функции позволяет избежать необходимости в глобальных или статических переменных, которые в конечном итоге причинят вам боль. Особенно, если у вас есть несколько вызовов из нескольких потоков.   -  person Michael Anderson    schedule 27.11.2014
comment
в каком-то статическом контейнере, например. может list   -  person M.M    schedule 27.11.2014
comment
@GrzegorzHerman Я считаю, что вам нужно найти место, где вы можете спрятать vector в каком-то (синхронизированном) контейнере, к которому можно получить доступ из обоих мест. Тогда нет необходимости в том, что вы делаете выше. И независимо от того, является ли выход temp за рамки UB, вы уже нарушаете контракт allocator::deallocate, передавая ему size, который не обязательно совпадает с переданным allocator::allocate.   -  person Praetorian    schedule 27.11.2014
comment
@Praetorian: именно поэтому я задаю этот вопрос   -  person Grzegorz Herman    schedule 27.11.2014
comment
Рассматривали ли вы, чтобы ваша функция возвращала unique_ptr<T[]> вместо vector<T>?   -  person Billy ONeal    schedule 27.11.2014
comment
Я не понимаю, в чем проблема. Пока vector не выходит за рамки. Что-то, что вы должны контролировать потоком кода. Вы можете сделать lib::startProcesing(input.data(), input.size); до тех пор, пока эта функция обещает не перераспределять или делать странные вещи со своим указателем. Кто звонит callback Кстати?   -  person alfC    schedule 27.11.2014


Ответы (3)


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

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

static std::map<T*, std::vector<T>*> registry;
void my_startProcessing(std::vector<T> * data) {
  registry.put(data->data(), data);
  lib::startProcesing(data->data(), data->size());
}

void my_callback(T * data, size_t length) {
  std::vector<T> * original = registry.get(data);
  delete original;
  registry.remove(data);
}

Теперь вы можете просто сделать

std::vector<T> * input = ...
my_startProcessing(input);

Но будьте осторожны! Плохие вещи произойдут, если вы добавите/удалите элементы во входные данные после того, как вы вызвали my_startProcessing - буфер, который есть в библиотеке, может стать недействительным. (Вам может быть разрешено изменять значения в векторе, так как я считаю, что это будет правильно записывать данные в to, но это будет зависеть и от того, что позволяет библиотека.)

Также это не работает, если T=bool, так как std::vector<bool>::data() не работает.

person Michael Anderson    schedule 27.11.2014
comment
Выглядит неплохо. Если я не найду способа избежать глобальных переменных, я добавлю немного std::mutex и std::unique_ptr, и все будет в порядке. Спасибо! - person Grzegorz Herman; 27.11.2014

Вы можете создать собственную сборку класса поверх вектора.

Ключевым моментом здесь является использование семантики перемещения в конструкторе SomeData.

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

Поскольку базовый тип данных будет массивом, вы можете вычислить начальный указатель и размер данных (см. SomeDataImpl.h ниже):

SomeData.h

#pragma once
#include <vector>

template<typename T>
class SomeData
{
    std::vector<T> thisData;

public:
    SomeData(std::vector<T> && other);

    const T* Start() const;
    size_t Size() const;
};

#include "SomeDataImpl.h"

SomeDataImpl.h

#pragma once

template<typename T>
SomeData<T>::SomeData(std::vector<T> && otherData) : thisData(std::move(otherData)) { }

template<typename T>
const T* SomeData<T>::Start() const {
    return thisData.data();
}

template<typename T>
size_t SomeData<T>::Size() const {
    return sizeof(T) * thisData.size();
}

Пример использования:

#include <iostream>
#include "SomeData.h"

template<typename T>
void Print(const T * start, size_t size) {
    size_t toPrint = size / sizeof(T);
    size_t printed = 0;

    while(printed < toPrint) {
        std::cout << *(start + printed) << ", " << start + printed << std::endl;
        ++printed;
    }
}

int main () {
    std::vector<int> ints;
    ints.push_back(1);
    ints.push_back(2);
    ints.push_back(3);

    SomeData<int> someData(std::move(ints));
    Print<int>(someData.Start(), someData.Size());

  return 0;
}
person d453    schedule 27.11.2014

Вы не можете сделать это каким-либо переносимым способом, но вы МОЖЕТЕ сделать это таким образом, который, вероятно, будет работать в большинстве реализаций C++. Этот код, кажется, работает после быстрого теста на VS 2017.

#include <iostream>

#include <vector>

using namespace std;

template <typename T>
T* HACK_stealVectorMemory(vector<T>&& toStealFrom)
{
    // Get a pointer to the vector's memory allocation
    T* vectorMemory = &toStealFrom[0];

    // Construct an empty vector in some stack memory using placement new
    unsigned char buffer[sizeof(vector<T>)];
    vector<T>* fakeVector = new (&buffer) vector<T>();

    // Move the memory pointer from toCopy into our fakeVector, which will never be destroyed.
    (*fakeVector) = std::move(toStealFrom);

    return vectorMemory;
}

int main()
{
    vector<int> someInts = { 1, 2, 3, 4 };
    cout << someInts.size() << endl;

    int* intsPtr = HACK_stealVectorMemory(std::move(someInts));

    cout << someInts.size() << endl;

    cout << intsPtr[0] << ", " << intsPtr[3] << endl;

    delete intsPtr;
}

Выход:

4
0
1, 4
person alvion    schedule 12.09.2019