boost — C ++ итераторы произвольного доступа для контейнеров с элементами, загружаемыми по требованию

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

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

SpecificMessage m;
FileReader fr;
fr.open("file.bin");
fr.moveTo(120); // Move to Message #120
fr.read(&m);    // Try deserializing as SpecificMessage

Сам по себе FileReader прекрасно работает. Поэтому мы подумали о добавлении поддержки итераторов, совместимых с STL: итератор с произвольным доступом, который предоставляет ссылки только для чтения на определенные сообщения. Используется следующим образом

for (auto iter = fr.begin<SpecificMessage>(); iter != fr.end<SpecificMessage>(); ++iter) {
// ...
}

Примечание: приведенное выше предполагает, что файл содержит только сообщения типа SpecificMessage. Мы использовали boost::iterator_facade упростить реализацию.

Теперь мой вопрос сводится к: как правильно реализовать итератор? поскольку FileReader фактически не содержит внутреннюю последовательность сообщений, а загружает их по запросу.

Что мы уже пробовали:

Хранение сообщения как члена итератора

Этот подход хранит сообщение в экземпляре итератора. Который отлично работает для простых случаев использования, но не подходит для более сложных применений. Например. std::reverse_iterator имеет операцию разыменования, которая выглядит следующим образом

 reference operator*() const
{  // return designated value
_RanIt _Tmp = current;
return (*--_Tmp);
}

Это нарушает наш подход, поскольку возвращается ссылка на сообщение временного итератора.

Делая ссылочный тип равным типу значения

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

pointer operator->() const {
return (&**this);
}

который разыскивает себя, вызывает оператор *, который затем возвращает копию временного файла и, наконец, возвращает адрес этого временного объекта.

Хранение сообщения извне

В качестве альтернативы я хотел бы сохранить внешнее сообщение:

SpecificMessage m;
auto iter = fr.begin<SpecificMessage>(&m);
// ...

что также кажется ущербным для

auto iter2 = iter + 2

который будет иметь оба iter2 а также iter указать на тот же контент.

3

Решение

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

Что касается файлов, отображаемых в память, то, похоже, это не то, что я хочу иметь, как вы могли бы предоставить для них итератор над SpecificMessages?

Ну, если ваше SpecificMessage является типом POD, вы могли бы просто перебирайте сырую память напрямую. Если нет, вы можете иметь помощника десериализации (как у вас уже есть) и использовать Boost transform_iterator делать десериализацию по требованию.

Обратите внимание, что мы можем сделать файл отображения памяти удалось, фактически это означает, что вы можете просто использовать его как обычную кучу и хранить все стандартные контейнеры. Это включает в себя основанные на узлах контейнеры (map<>контейнеры динамического размера (например, vector<>) в дополнение к контейнерам фиксированного размера (array<>) — и любые их комбинации.

Вот демо, которое занимает простое SpecificMessage которая содержит строку и (де) дерилизует ее непосредственно в разделяемую память:

using blob_t       = shm::vector<uint8_t>;
using shared_blobs = shm::vector<blob_t>;

Часть, которая вас интересует, будет потребляющей частью:

bip::managed_mapped_file mmf(bip::open_only, DBASE_FNAME);
shared_blobs* table = mmf.find_or_construct<shared_blobs>("blob_table")(mmf.get_segment_manager());

using It = boost::transform_iterator<LazyLoader<SpecificMessage>, shared_blobs::const_reverse_iterator>;

// for fun, let's reverse the blobs
for (It first(table->rbegin()), last(table->rend()); first < last; first+=13)
std::cout << "blob: '" << first->contents << "'\n";

// any kind of random access is okay, though:
auto random = rand() % table->size();
SpecificMessage msg;
load(table->at(random), msg);
std::cout << "Random blob #" << random << ": '" << msg.contents << "'\n";

Таким образом, это печатает каждое 13-е сообщение в обратном порядке, за которым следует случайный блоб.

Полная демонстрация

Образец онлайн использует строки источников в качестве «сообщений».

Жить на Колиру

#include <boost/interprocess/file_mapping.hpp>
#include <boost/interprocess/managed_mapped_file.hpp>
#include <boost/container/scoped_allocator.hpp>
#include <boost/interprocess/containers/vector.hpp>
#include <iostream>

#include <boost/iterator/transform_iterator.hpp>
#include <boost/range/iterator_range.hpp>

static char const* DBASE_FNAME = "database.map";

namespace bip = boost::interprocess;

namespace shm {
using segment_manager = bip::managed_mapped_file::segment_manager;
template <typename T> using allocator = boost::container::scoped_allocator_adaptor<bip::allocator<T, segment_manager> >;
template <typename T> using vector    = bip::vector<T, allocator<T> >;
}

using blob_t       = shm::vector<uint8_t>;
using shared_blobs = shm::vector<blob_t>;

struct SpecificMessage {
// for demonstration purposes, just a string; could be anything serialized
std::string contents;

// trivial save/load serialization code:
template <typename Blob>
friend bool save(Blob& blob, SpecificMessage const& msg) {
blob.assign(msg.contents.begin(), msg.contents.end());
return true;
}

template <typename Blob>
friend bool load(Blob const& blob, SpecificMessage& msg) {
msg.contents.assign(blob.begin(), blob.end());
return true;
}
};

template <typename Message> struct LazyLoader {
using type = Message;

Message operator()(blob_t const& blob) const {
Message result;
if (!load(blob, result)) throw std::bad_cast(); // TODO custom excepion
return result;
}
};

///////
// for demo, create some database contents
void create_database_file() {
bip::file_mapping::remove(DBASE_FNAME);
bip::managed_mapped_file mmf(bip::open_or_create, DBASE_FNAME, 1ul<<20); // Even sparse file size is limited on Coliru

shared_blobs* table = mmf.find_or_construct<shared_blobs>("blob_table")(mmf.get_segment_manager());

std::ifstream ifs("main.cpp");
std::string line;
while (std::getline(ifs, line)) {
table->emplace_back();
save(table->back(), SpecificMessage { line });
}

std::cout << "Created blob table consisting of " << table->size() << " blobs\n";
}

///////

void display_random_messages() {
bip::managed_mapped_file mmf(bip::open_only, DBASE_FNAME);
shared_blobs* table = mmf.find_or_construct<shared_blobs>("blob_table")(mmf.get_segment_manager());

using It = boost::transform_iterator<LazyLoader<SpecificMessage>, shared_blobs::const_reverse_iterator>;

// for fun, let's reverse the blobs
for (It first(table->rbegin()), last(table->rend()); first < last; first+=13)
std::cout << "blob: '" << first->contents << "'\n";

// any kind of random access is okay, though:
auto random = rand() % table->size();
SpecificMessage msg;
load(table->at(random), msg);
std::cout << "Random blob #" << random << ": '" << msg.contents << "'\n";
}

int main()
{
#ifndef CONSUMER_ONLY
create_database_file();
#endif

srand(time(NULL));
display_random_messages();
}
2

Другие решения

У вас есть проблемы, потому что ваш итератор не соответствует требованиям прямого итератора. В частности:

  • *i должна быть lvalue ссылка на value_type или же const value_type ([Forward.iterators] /1.3)
  • *i не может быть ссылкой на объект, сохраненный в самом итераторе, из-за требования, что два итератора равны, если и только если они связаны с одним и тем же объектом ([forward.iterators] / 6)

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


РЕДАКТИРОВАТЬ: Следующее предлагаемое решение ужасно сломано, в том, что разыменование временного итератора возвращает ссылку на объект, который может не жить, пока ссылка не используется. Например, после auto& foo = *(i + 1); объект, на который ссылается foo возможно, был освобожден. Реализация reverse_iterator ссылка в OP вызовет ту же проблему.

Я бы предложил разделить ваш дизайн на два класса: FileCache который содержит файловые ресурсы и кэш загруженных сообщений, и FileCache::iterator который содержит номер сообщения и лениво извлекает его из FileCache когда разыменовывается. Реализация может быть чем-то таким простым, как хранение контейнера weak_ptr<Message> в FileCache и shared_ptr<Message> в итераторе: Простая демонстрация

2

Я должен признать, что, возможно, я не до конца понимаю проблемы, с которыми вы сталкиваетесь, удерживая текущее СООБЩЕНИЕ в качестве члена Iter. Я бы связал каждый итератор с FileReader, из которого он должен читать, и реализовал его как легкую инкапсуляцию индекса чтения для FileReader: 🙁 read | moveTo). Наиболее важным методом перезаписи является boost::iterator_facade<...>::advance(...) который изменяет текущий индекс и пытается извлечь новое СООБЩЕНИЕ из FileReader. Если это не удастся, он помечает итератор как недействительный, а разыменование завершается ошибкой.

template<class MESSAGE,int STEP>
class message_iterator;

template<class MESSAGE>
class FileReader {
public:
typedef message_iterator<MESSAGE, 1> const_iterator;
typedef message_iterator<MESSAGE,-1> const_reverse_iterator;

FileReader();
bool open(const std::string  & rName);
bool moveTo(int n);
bool read(MESSAGE &m);

// get the total count of messages in the file
// helps us to find end() and rbegin()
int getMessageCount();

const_iterator begin() {
return const_iterator(this,0);
}
const_iterator end() {
return const_iterator(this,getMessageCount());
}
const_reverse_iterator rbegin() {
return const_reverse_iterator(this,getMessageCount()-1);
}
const_reverse_iterator rend() {
return const_reverse_iterator(this,-1);
}
};

// declaration of message_iterator moving over MESSAGE
// STEP is used to specify STEP size and direction (e.g -1 == reverse)
template<class MESSAGE,int STEP=1>
class message_iterator
: public boost::iterator_facade<
message_iterator<MESSAGE>
, const MESSAGE
, boost::random_access_traversal_tag
>
{
typedef  boost::iterator_facade<
message_iterator<MESSAGE>
, const MESSAGE
, boost::random_access_traversal_tag
> super;

public:
// constructor associates an iterator with its FileReader and a given position
explicit message_iterator(FileReader<MESSAGE> * p=NULL,int n=0): _filereader(p),_idx(n),_valid(false)    {
advance(0);
}
bool equal(const message_iterator & i) const {
return i._filereader == _filereader && i._idx == _idx;
}
void increment() {
advance(+1);
}
void decrement() {
advance(-1);
}

// overwrite with central functionality. Move to a given relative
// postion and check wether the position can be read. If move/read
// fails we flag the iterator as incalid.

void advance(int n) {
_idx += n*STEP;
if(_filereader!=NULL) {
if( _filereader->moveTo( _idx ) && _filereader->read(_m)) {
_valid = true;
return;
}
}
_valid = false;
}
// Return a ref to the currently cached MESSAGE. Throw
// an acception if positioning at this location in advance(...) failes.
typename super::reference dereference() const {
if(!_valid) {
throw std::runtime_error("access to invalid pos");
}
return _m;
}

private:
FileReader<MESSAGE> * _filereader;
int                   _idx;
bool                  _valid;
MESSAGE               _m;
};
0

Увеличить PropertyMap

Вы можете избежать написания основной части кода, используя Boost PropertyMap:

Жить на Колиру

#include <boost/property_map/property_map.hpp>
#include <boost/property_map/function_property_map.hpp>

using namespace boost;

struct SpecificMessage {
// add some data
int index; // just for demo
};

template <typename Message>
struct MyLazyReader {
typedef Message type;
std::string fname;

MyLazyReader(std::string fname) : fname(fname) {}

Message operator()(size_t index) const {
Message m;
// FileReader fr;
// fr.open(fname);
// fr.moveTo(index);     // Move to Message
// fr.read(&m);          // Try deserializing as SpecificMessage
m.index = index; // just for demo
return m;
}
};

#include <iostream>

int main() {

auto lazy_access = make_function_property_map<size_t>(MyLazyReader<SpecificMessage>("file.bin"));

for (int i=0; i<10; ++i)
std::cout << lazy_access[rand()%256].index << "\n";
}

Пример вывода

103
198
105
115
81
255
74
236
41
205

Использование файлов с отображенной памятью

Вы можете хранить карту индекса -> BLOB-объектов в общем vector<array<byte, N>>, flat_map<size_t, std::vector<uint8_t> > или похожие.

Итак, теперь вам нужно только десериализовать из myshared_map[index].data() (begin() а также end() если размер BLOB меняется)

0
По вопросам рекламы [email protected]