【问题标题】:C++ random access iterators for containers with elements loaded on demandC++ 随机访问迭代器,用于按需加载元素的容器
【发布时间】:2015-01-05 18:17:33
【问题描述】:

我目前正在处理一个需要从文件加载消息的小项目。消息按顺序存储在文件中,文件可能会变得很大,因此将整个文件内容加载到内存中是没有回报的。

因此我们决定实现一个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);
 }

这破坏了我们的方法,因为返回了对来自临时迭代器的消息的引用。

使引用类型等于值类型

cmets 中的@DDrmmr 建议使引用类型等于值类型,以便返回内部存储对象的副本。但是,我认为这对于将 -> 运算符实现为

的反向迭代器无效
pointer operator->() const {
  return (&**this);
}

它自己解引用,调用 *operator 然后返回一个临时的副本,最后返回这个临时的地址。

在外部存储消息

另外,我想在外部存储消息:

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

这似乎也有缺陷

auto iter2 = iter + 2

iter2iter 都指向相同的内容。

【问题讨论】:

  • 承诺是我们用来表示数据将被承诺但不一定当前可用的概念。我是否可以建议调整迭代器以返回承诺或类似概念以在文件中懒惰地移动?
  • 为什么不按值返回消息?
  • @MorphingDragon 不确定您的意思。你有一些参考资料吗?
  • @DDrmmr 好主意。但我刚刚发现反向迭代器执行以下指针 operator->() const {return (&**this);} 这意味着它调用自己的 *operator 生成值副本,然后将地址返回给它。跨度>
  • @ChristophHeindl 你知道消息的数量吗?

标签: c++ boost iterator std random-access


【解决方案1】:

我不得不承认,作为 Iter 的成员,我可能无法完全理解您在持有当前 MESSAGE 时遇到的麻烦。我会将每个迭代器与它应该读取的 FileReader 相关联,并将其实现为 FileReader::(read|moveTo) 的读取索引的轻量级封装。最重要的 overwtite 方法是 boost::iterator_facade&lt;...&gt;::advance(...),它会修改当前索引并尝试从 FileReader 中提取新的 MESSAGE。如果失败,它会将迭代器标记为无效并且取消引用将失败。

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; 
}; 

【讨论】:

  • 感谢您的意见。这完全模仿了我们目前所拥有的,除了您如何实现反向迭代器。我正在使用 std::reverse_iterator 适配器。我所有的问题都源于它的实现(我想这与 STL 一致),因此我的迭代器有缺陷。我知道我可以使用您提供的解决方案 (STEP=-1) 来规避这个问题,但我认为其他符合 STL 的事情也会失败。
【解决方案2】:

您遇到问题是因为您的迭代器不符合前向迭代器要求。具体来说:

  • *i 必须是对 value_typeconst value_type 的左值引用 ([forward.iterators]/1.3)
  • *i 不能是对存储在迭代器本身中的对象的引用,因为要求两个迭代器当且仅当它们绑定到同一个对象时才相等([forward.iterators]/6)

是的,这些要求让人头疼,是的,这意味着像 std::vector&lt;bool&gt;::iterator 这样的东西不是随机访问迭代器,即使某些标准库实现错误地声称它们是。


编辑:以下建议的解决方案被严重破坏,因为取消引用临时迭代器会返回对对象的引用,该对象在使用引用之前可能不会存在。例如,在auto&amp; foo = *(i + 1); 之后,foo 引用的对象可能已被释放。 OP中引用的reverse_iterator的实现也会导致同样的问题。

我建议你将你的设计分成两个类:FileCache 保存文件资源和已加载消息的缓存,FileCache::iterator 保存消息编号并从@ 中懒惰地检索它取消引用时为 987654332@。实现可以很简单,例如在 FileCache 中存储一个 weak_ptr&lt;Message&gt; 的容器,在迭代器中存储一个 shared_ptr&lt;Message&gt;Simple demo

【讨论】:

  • 这就是我所怀疑的。感谢您对要求的澄清。也感谢演示。我会接受这个答案。还有一个附带问题。
  • 还有一个注意事项。在示例中,您不认为只有 const 迭代器才有意义吗?
  • @ChristophHeindl 是的,尽管可以为shared_ptr&lt;Message&gt;s 实现一个自定义删除器,它确实写回支持文件。我将把它作为练习留给读者;)
  • @ChristophHeindl 您还应该注意,mutable 成员的使用存在线程安全问题,我特意避免使用 std::make_shared,以便 weak_ptr 控制块不会保持Message 内存分配保持不变。
  • @ChristophHeindl 坏消息,我是个白痴,我建议的方法行不通。
【解决方案3】:

提升属性映射

您可以避免使用 Boost PropertyMap 编写大量代码:

Live On Coliru

#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&lt;array&lt;byte, N&gt;&gt;flat_map&lt;size_t, std::vector&lt;uint8_t&gt; &gt; 或类似文件中。

所以,现在您只需从 myshared_map[index].data() 反序列化(begin()end(),以防 BLOB 大小发生变化)

【讨论】:

  • 您好,感谢您的反馈。实际上 function_property_map 避免了大部分 C++ 代码。但是,我们项目中的辅助函数可以使用范围内的迭代器。由于我想使用该功能,仍然希望有一个迭代器。就内存映射文件而言,这似乎不是我想要的,因为您将如何为它们提供针对 SpecificMessages 的迭代器?
  • @ChristophHeindl 我冒昧地在另一个答案中证明了这一点:using Memory Mapped File
【解决方案4】:

正如我在其他答案中所暗示的,您可以考虑使用内存映射文件。在你问的评论中:

就内存映射文件而言,这似乎不是我想要的,因为您将如何为它们提供针对 SpecificMessages 的迭代器?

好吧,如果您的 SpecificMessage 是 POD 类型,您可以只是直接迭代原始内存。如果没有,您可以拥有一个反序列化助手(就像您已经拥有的那样)并使用 Boost transform_iterator 按需进行反序列化。

请注意,我们可以管理内存映射文件,这实际上意味着您可以将其用作常规堆,并且可以存储所有标准容器。这包括基于节点的容器(例如map&lt;&gt;)、动态大小的容器(例如vector&lt;&gt;)以及固定大小的容器(array&lt;&gt;)——以及它们的任意组合。

这是一个演示,它采用包含字符串的简单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 条消息,然后是随机 blob。

完整演示

在线示例使用来源的行作为“消息”。

Live On Coliru

#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();
}

【讨论】:

  • 非常感谢。不知道 boost 中的内存映射文件支持。
猜你喜欢
  • 2019-12-06
  • 2020-04-23
  • 2022-11-14
  • 2016-10-16
  • 1970-01-01
  • 1970-01-01
  • 2017-09-02
  • 2012-08-19
  • 1970-01-01
相关资源
最近更新 更多