Используйте std :: vector <double>для доступа к данным, управляемым std :: unique_ptr - PullRequest
2 голосов
/ 19 июня 2020

У меня есть сложный класс, который содержит большой блок данных типа double[2], управляемый интеллектуальным указателем, например: std::unique_ptr<double[2]> m_data; Я не могу изменить тип структуры данных.

Я использую библиотека, которая дает мне функцию со следующей подписью: bool func_in_lib(std::vector<double>& data, double& res). Я не могу изменить подпись этой функции.

Я хочу передать данные, управляемые unique_ptr, функции, ожидающей vector<double>&, без разрыва соединения с моим сложным классом. Я хочу, чтобы функция работала непосредственно на моем m_data, а не копировала данные в std::vector<double> и не копировала их обратно в свой сложный класс, потому что мне приходится делать это много раз.

Is есть ли способ сделать это?


Вот код, который охватывает семанти c, которые я хочу иметь. Строка кода, которая меня беспокоит:

vector<double> access_vec = /* give access to my_data via vector interface */;


#include <iostream>
#include <memory>
#include <vector>

using namespace std;

//--------------------------------------------------------------------------//
//--- This function is given, I cannot change its signature.
bool
func_in_lib(std::vector<double>& data, double& res) {
  //--- check some properties of the vector
  if (data.size() < 10)
    return false;
  //--- do something magical with the data
  for (auto& d : data)
    d *= 2.0;
  res = 42.0;
  return true;
}

//--------------------------------------------------------------------------//
struct DataType {
  double a = 1.0;
  double b = 2.0;
  double c = 3.0;
};

//--------------------------------------------------------------------------//
ostream&
operator<<(ostream& out, const DataType& d) {
  out << d.a << " " << d.b << " " << d.c << endl;
  return out;
}

//--------------------------------------------------------------------------//
int
main(int argc, char const* argv[]) {
  int count = 20;
  //--- init and print my data
  unique_ptr<DataType[]> my_data = make_unique<DataType[]>(count);
  for (int i = 0; i < count; ++i)
    cout << my_data.get()[i];
  //---
  double         result     = 0.0;
  vector<double> access_vec = /* give access to my_data via vector interface */;
  func_in_lib(access_vec, result);

  return 0;
}

Ответы [ 3 ]

1 голос
/ 19 июня 2020

tl; dr: Невозможно стандартным способом.

На самом деле почти возможно, но std::allocator ограничения блокируют ваш путь. Позвольте мне объяснить.

  • An std::vector «владеет» памятью, которую он использует для хранения элементов: вектор имеет право на delete[] память (например, при уничтожении, или разрушение после перемещения, или .resize(), или push_back и c.) и перераспределить в другом месте. Если вы хотите сохранить право собственности на свой unique_ptr, вы не можете этого допустить. И хотя это правда, что ваша макетная реализация func_in_lib() ничего из этого не делает - ваш код не может делать эти предположения, потому что он должен обслуживать объявление функции, а не ее тело.

Но предположим, что вы готовы немного изменить правила и предположить, что вектор не будет заменять выделенную ему память во время работы. Это законно в том смысле, что - если вы смогли каким-то образом передать память для использования вектора, и она заменила область памяти, вы могли бы обнаружить это, когда возвращается func_in_lib(), и затем либо исправить что-то в unique_ptr, либо генерировать исключение (в зависимости от того, содержат ли другие места в вашем коде указатель на сброшенную память). Или - предположим, что func_in_lib() взял const std::vector<double[2]>& вместо неконстантной ссылки. Наш путь все равно будет заблокирован. Почему?

  • std::vector управляет памятью через объект-распределитель. Распределитель - это шаблон, поэтому теоретически вы можете использовать вектор, в котором распределитель делает все, что вы хотите - например, начиная с предварительно выделенной памяти (которую вы ей предоставляете - с unique_ptr::get(), и отказываясь когда-либо перераспределять любую память, например, выбрасывая исключение. А поскольку один из конструкторов std::vector принимает распределитель соответствующего типа, вы можете построить желаемый распределитель, создать с ним вектор и передать ссылку на этот вектор.

Но, увы, ваша библиотека жестока. func_in_lib не является шаблоном и может принимать только параметр шаблона по умолчанию для своего распределителя: std::allocator.

  • Распределитель по умолчанию, используемый для std::vector и другие стандартные библиотечные контейнеры - это std::allocator. Сейчас, на мой взгляд, аллокаторы - это вообще неправильная идея; но std::allocator особенно раздражает. В частности, не может быть сконструирован с использованием уже существующая область памяти для использования ; он всегда хранит только память, которую он выделил сам - никогда память, которую вы дали i t.

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

Итак, что делать?

  1. Вариант 1: Ваш хак:

    • Определите конкретную схему std::vector в вашей системе
    • Вручную установите значения полей на что-нибудь полезное
    • Используйте reinterpret_cast<std::vector>() на ваших необработанных данных.
  2. Вариант 2: malloc() и free() хуки (если вы используете Unix -подобную систему и / или используя скомпилированный, который использует lib c)

    • См.: Использование Mallo c Hooks

      идея состоит в том, чтобы обнаружить new[] вызовите из созданного вами std::vector и предоставьте ему вашу собственную unique_ptr -управляемую память вместо того, чтобы фактически выделять что-либо. А когда вектор просит освободить память (например, при уничтожении), вы ничего не делаете.

  3. Переключайте библиотеки. Библиотека, выставляющая func_in_lib, написана плохо. Если это не очень нишевая библиотека, я уверен, что есть альтернативы получше. Фактически, peharps, вы могли бы лучше написать его самостоятельно.

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

1 голос
/ 19 июня 2020

Со своим коллегой я нашел два решения, которые решают мою проблему.

Решение 1 - Хакерское

Идея состоит в том, чтобы использовать структуру базовой реализации std::vector<double>, который состоит из в моем случае из 3 элементов, содержащих 3 указателя на данные вектора.

  1. начальный адрес раздела данных
  2. конечный адрес раздела данных
  3. адрес текущей максимальной емкости раздела данных

Итак, я создаю struct, содержащий эти три адреса, и использую reinterpret_cast до std::vector. Это работает с текущей реализацией std::vector на моей машине. Эта реализация может отличаться в зависимости от установленной версии STL.

Приятно то, что я могу использовать интерфейс std::vector, не создавая его. Мне также не нужно копировать данные в std::vector. Я также мог взять часть исходных данных, хранящихся в моем сложном классе. Я могу управлять управляемой частью с помощью указателей, которые я отправляю в структуру.

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

#include <iostream>
#include <memory>
#include <vector>

using namespace std;

//--------------------------------------------------------------------------//
//--- This function is given, I cannot change its signature.
bool
func_in_lib(std::vector<double>& data, double& res) {
  //--- check some properties of the vector
  if (data.size() < 10)
    return false;
  //--- do something magical with the data
  for (auto& d : data)
    d *= 2.0;

  res = 42.0;
  return true;
}

//--------------------------------------------------------------------------//
struct DataType {
  double a = 1.0;
  double b = 2.0;
  double c = 3.0;
};

//--------------------------------------------------------------------------//
ostream&
operator<<(ostream& out, const DataType& d) {
  out << d.a << " " << d.b << " " << d.c << endl;
  return out;
}

//--------------------------------------------------------------------------//
int
main(int argc, char const* argv[]) {
  int count = 20;
  //--- init and print my data
  unique_ptr<DataType[]> my_data = make_unique<DataType[]>(count);
  for (int i = 0; i < count; ++i)
    cout << my_data.get()[i];
  
  //--------------------------------------------------------------------------//
  // HERE STARTS THE UGLY HACK, THAT CAN BE ERROR-PRONE BECAUSE IT DEPENDS ON
  // THE UNDERLYING IMPLEMENTATION OF std::vector<T>
  //--------------------------------------------------------------------------//
  struct VecAccess {
    double* start = nullptr; // address to the start of the data
    double* stop0 = nullptr; // address to the end of the data
    double* stop1 = nullptr; // address to the capacity of the vector
  };

  //---
  DataType*       p_data = my_data.get();
  VecAccess       va{ &(p_data[0].a),                //points at the 'front' of the vector
                      &(p_data[count - 1].c) + 1,    //points at the 'end' of the vector
                      &(p_data[count - 1].c) + 1 };
  vector<double>* p_vec_access = reinterpret_cast<vector<double>*>(&va);
  //--------------------------------------------------------------------------//
  // HERE ENDS THE UGLY HACK.
  //--------------------------------------------------------------------------//

  //---
  double dummy = 0.0;   // this is only relevant for the code used as minimum example
  func_in_lib(*p_vec_access, dummy);

  //--- print the modified data
  for (int i = 0; i < count; ++i)
    cout << my_data.get()[i];

  return 0;
}

Обновление: Анализ кода ассемблера второго решения показывает, что копирование содержимого выполняется, даже если конструктор копирования объектов данных не вызывается. Процесс копирования происходит на уровне машинного кода.

Решение 2. Перемещение semanti c

Для этого решения я должен отметить Move-Constructor DataType с помощью noexcept. Ключевая идея - не рассматривать массив DataType[] как std::vector<double>. Вместо этого мы рассматриваем std::vector<double> как std::vector<DataType>. Затем мы можем переместить данные в этот вектор (без копирования), отправить их в функцию и потом переместить обратно.

Данные не копируются, а перемещаются std::vector, что быстрее. Также актуально для моего случая, я снова могу взять часть исходных данных, хранящихся в моем сложном классе. Недостаток этого решения: мне нужно создать дополнительное хранилище для перемещенных данных правильного размера.

#include <iostream>
#include <memory>
#include <utility>
#include <vector>

using namespace std;

//--------------------------------------------------------------------------//
//--- This function is given, I cannot change its signature.
bool
func_in_lib(std::vector<double>& data, double& res) {
  //--- check some properties of the vector
  if (data.size() < 10)
    return false;
  //--- do something magical with the data
  for (auto& d : data)
    d *= 2.0;

  res = 42.0;
  return true;
}

//--------------------------------------------------------------------------//
class DataType {
public:
  double a = 1.0;
  double b = 2.0;
  double c = 3.0;

  // clang-format off
  DataType() = default;
  DataType(DataType const&) = default;
  DataType(DataType&&) noexcept = default;
  DataType& operator=(DataType const&) = default;
  DataType& operator=(DataType&&) noexcept  = default;
  ~DataType()  = default;
  // clang-format on
};

//--------------------------------------------------------------------------//
ostream&
operator<<(ostream& out, const DataType& d) {
  out << d.a << " " << d.b << " " << d.c << endl;
  return out;
}

//--------------------------------------------------------------------------//
int
main(int argc, char const* argv[]) {
  int count = 20;
  //--- init and print my data
  unique_ptr<DataType[]> my_data = make_unique<DataType[]>(count);
  for (int i = 0; i < count; ++i)
    cout << my_data.get()[i];
  //---
  vector<double> double_vec;
  double_vec.reserve(count * 3);
  //--- here starts the magic stuff
  auto& vec_as_datatype = *reinterpret_cast<vector<DataType>*>(&double_vec);
  auto* start_mv        = &(my_data.get()[0]);
  auto* stop_mv         = &(my_data.get()[count]) + 1;
  //--- move the content to the vec
  move(start_mv, stop_mv, back_inserter(vec_as_datatype));
  //--- call the external func in the lib
  double dummy = 0.0; // is only needed for the code of the example
  func_in_lib(double_vec, dummy);
  //--- move the content to back
  move(begin(vec_as_datatype), end(vec_as_datatype), start_mv);
  //--- print modified the data
  for (int i = 0; i < count; ++i)
    cout << my_data.get()[i];
}
0 голосов
/ 30 июня 2020

Это не разумный ответ, но никто не упомянул (потому что он, безусловно, не отвечает напрямую на ваш вопрос) C ++ 17 polymorphi c allocator с std :: pmr :: vector в том смысле, что они могут легко сделать половину работы.

Но , к сожалению, невозможно вернуться к обычному std :: vector

Я также наткнулся на статью Блог по кодированию Bartek, из которого я украл фрагмент кода ниже:

#include <iostream>
#include <memory_resource>   // pmr core types
#include <vector>            // pmr::vector
#include <cctype>

template <typename T> void MyToUpper(T& vec)    {
    for(auto & cr:vec)
        cr = std::toupper(cr);
}

//https://www.bfilipek.com/2020/06/pmr-hacking.html

int main() {
    char buffer[64] = {}; // a small buffer on the stack
    std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_');
    std::cout << buffer << "\n\n";

    std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};

    std::pmr::vector<char> vec{ &pool };
    for (char ch = 'a'; ch <= 'z'; ++ch)
        vec.push_back(ch);
        
    std::cout << buffer << "\n\n";
    
    MyToUpper(vec);
    
    std::cout << buffer << '\n';
}

с потенциальным результатом под coliru (примечание: c ++ 17)

_______________________________________________________________

aababcdabcdefghabcdefghijklmnopabcdefghijklmnopqrstuvwxyz______

aababcdabcdefghabcdefghijklmnopABCDEFGHIJKLMNOPQRSTUVWXYZ______

В статье упоминалось, что часть мусора (aababcdabcdefghabcdefghijklmnop) возникает из-за перераспределения векторных данных при росте.

Но что Здесь интересно то, что операция, выполненная с вектором, действительно была выполнена в исходном буфере (abcdefghijklmnopqrstuvwxyz => ABCDEFGHIJKLMNOPQRSTUVWXYZ)

К сожалению, std::pmr::vector не подходит для вашей функции func_in_lib(std::vector<double>& data, double& res)

I думаю, что вы купили библиотеку и не имеете доступа к коду и не можете перекомпилировать ее, но, наоборот, вы можете использовать шаблоны или, может быть, просто попросите своего провайдера добавить с помощью std :: pmr :: vector; в начале своего кода ...

...