【问题标题】:Create multiple indexes into a large collection of objects with smart pointers使用智能指针为大量对象创建多个索引
【发布时间】:2015-05-08 21:14:19
【问题描述】:

我正在为大量对象创建多个索引(即使用不同键的索引)。对象可以改变,集合可以缩小和增长。到目前为止我的想法:

保留多个指向对象的指针的集合。 使用 set 代替 map 以获得更好的封装。 使用 unordered_set 可以很好地处理大型数据集。 理想情况下,指针应该都是某种形式的智能指针。

我可以从一个管理所有分配的唯一_ptrs 主集合和使用“原始”指针的二级索引开始相当容易(我将暂时省略支持函数,但请注意索引是multiset 作为它的键在集合中不会是唯一的):

typedef boost::unordered_set< boost::unique_ptr<MyObject>,myobject_hash,myobjects_equal > MyObjects;
typedef boost::unordered_multiset<const MyObject*,myobject_index2_hash,myobject_index2_equal > MyObjectsIndex2;

用法很简单:

MyObjects my_objects;
MyObjectsIndex2 my_objects_index2;

auto it_mo = my_objects.insert(
    boost::unique_ptr<MyObject>(
        new MyObject(...)
    )
);
const MyObject* p_mo = it_mo.first->get();
my_objects_index2.insert(p_mo);

我正在考虑付出额外的努力,将索引对原始指针的使用替换为对主集合的 unique_ptrs 的 const 引用。我不确定我能不能,至少不容易。我想我会问其他人是否已经走这条路,或者有其他建议。

更新

到目前为止的经验教训:

  1. 数据存储类很酷
  2. reference_wrappers 很酷
  3. 带有“key”对象数据存储成员 var 的 xx_set 比 xx_map 更节省空间。但是...您不能轻易地将 unique_ptr 用作 c++11 中的键。 c++14 显然可能通过std::set&lt;Key&gt;::find 具有更好的设置功能。有关详细信息,请参阅here。因此,就目前而言,管理原始分配的数据存储似乎比尝试强制使用 unique_ptr 作为设置键或使用映射增加键空间存储更有意义。
  4. 记住在对象的生命周期内强制键值为 const(使用构造函数中提供的 const 值)

【问题讨论】:

  • 对唯一指针的引用将不起作用,因为这些指针会随着主存储结构的增长而重新分配(除非它以某种方式被写入以保持对象到位)。除非您愿意存储对象的多个副本,否则您无法真正绕过这里需要第二层间接,这会浪费大量空间。无论如何,我认为在主存储中使用共享指针会更好,在索引中使用弱指针。
  • 谢谢 Wug,关于移动 unique_ptrs 的要点,这将是另一个需要管理的细节。是的,共享+弱指针方法会起作用,但会以引用计数为代价。这还不错。我对此有点着迷,因为它是如此基础。我认为原始指针方法似乎还可以……我仍在考虑它……
  • 让我看看能不能做点有用的事。

标签: c++ c++11 c++14


【解决方案1】:

这是一种方法。

可编译示例:

#include <map>
#include <vector>
#include <set>
#include <string>
#include <functional>
#include <memory>
#include <iostream>

struct Thing {
    Thing(std::string name, int value)
    : _name { std::move(name) }
    , _value { value }
    {}

    const std::string& name() const {
        return _name;
    }

    void write(std::ostream& os) const {
        os << "{ " << _name << " : " << _value << " }";
    }    
private:
    std::string _name;
    int _value;
};

inline std::ostream& operator<<(std::ostream& os, const Thing& t) {
    t.write(os);
    return os;
}

struct multi_index
{
    using multi_by_name_index = std::multimap<std::string, std::reference_wrapper<Thing>>;

    void add_thing(std::string name, int value) {

        // todo: checks to ensure that indexes won't be violated

        // add a new thing to the main store
        _main_store.emplace_back(new Thing{std::move(name), value});

        // store a reference to it in each index
        auto& new_thing = *(_main_store.back().get());
        _name_index.emplace(new_thing.name(), new_thing);
    }

    using multi_by_name_range = std::pair<multi_by_name_index::const_iterator, multi_by_name_index::const_iterator>;
    multi_by_name_range get_all_by_name(const std::string name) const
    {
        return _name_index.equal_range(name);
    }

private:
    std::vector<std::unique_ptr<Thing>> _main_store;
    std::multimap<std::string, std::reference_wrapper<Thing>> _name_index;
};

using namespace std;

int main()
{
    multi_index mi;

    mi.add_thing("bob", 8);
    mi.add_thing("ann", 4);
    mi.add_thing("bob", 6);

    auto range = mi.get_all_by_name("bob");
    for( ; range.first != range.second ; ++range.first) {
        cout << range.first->second << endl;
    }

   return 0;
}

预期输出:

{ bob : 8 }                                                                                                                             
{ bob : 6 }  

【讨论】:

  • 还有std::unordered_multimap,这可能更好。另外,鉴于std::unique_ptrs 对调用者完全隐藏,使用它们有什么意义吗?为什么不直接使用std::unordered_set&lt;Thing&gt;,而不是std::vector&lt;std::unique_ptr&lt;Thing&gt;&gt;
  • 无序:是的,没关系。你只需要确保有一个 operator== 和一个散列函数。设为主店?这意味着 Thing 必须是唯一的,并且必须提供
  • 感谢 reference_wrappers 提示,太棒了!我想这是“表明”分配在其他地方处理的最佳方式。你知道reference_wrapper 是否比使用原始指针有任何开销吗?
  • 不客气。不,根本没有开销。在内部它是一个指针。引用包装器为它提供了引用接口,因此它在标准算法等方面表现良好
【解决方案2】:

我认识到您的用例可能与我为示例设计的用例不同,如果没有更多细节,我将无法做出匹配的用例(我还认为,如果您有很多细节您将能够自己找到解决方案。

#include <iostream>
#include <map>
#include <set>
#include <memory>
#include <stdexcept>

using namespace std;

class Thing
{
public:
    Thing() = default;
    Thing(const Thing &other) = default;
    Thing(int i, string p, string d) : id(i), desc(d), part(p) {}

    int    id;
    string desc;
    string part;
};

ostream &operator<<(ostream &out, const Thing &t)
{
    if (&t == NULL) out << "(NULL)"; // don't judge me
    else out << t.id << ": " << t.part << " (" << t.desc << ")";
}

class Datastore
{
public:
    Datastore() = default;
    shared_ptr<const Thing> Add(const Thing &t)
    {
        if (!(index_bydesc.find(t.desc) == index_bydesc.end() &&
              index_bypart.find(t.part) == index_bypart.end() &&
              index_byid.find(t.id) == index_byid.end()))
            throw runtime_error("Non-unique insert");
        shared_ptr<const Thing> newt = make_shared<const Thing>(t);
        weak_ptr<const Thing> weak = weak_ptr<const Thing>(newt);
        index_bydesc[newt->desc] = weak;
        index_bypart[newt->part] = weak;
        index_byid[newt->id] = weak;
        store.insert(newt);
        return newt;
    }

    void Remove(const Thing &t)
    {
        shared_ptr<const Thing> p = FindBy_Desc(t.desc);
        store.erase(p);
        index_bydesc.erase(p->desc);
        index_bypart.erase(p->part);
        index_byid.erase(p->id);
    }

    shared_ptr<const Thing> FindBy_Desc(string desc)
    {
        map<string, weak_ptr<const Thing> >::iterator iter = index_bydesc.find(desc);
        if (iter == index_bydesc.end()) return shared_ptr<const Thing>();
        return iter->second.lock();
    }

    // index accessors for part and quantity omitted

private:
    std::set<shared_ptr<const Thing> > store;

    std::map<string, weak_ptr<const Thing> > index_bydesc;
    std::map<string, weak_ptr<const Thing> > index_bypart;
    std::map<int, weak_ptr<const Thing> > index_byid;
};

int main() {
    Datastore d;
    d.Add(Thing(1, "TRNS-A", "Automatic transmission"));
    d.Add(Thing(2, "SPKPLG", "Spark plugs"));
    d.Add(Thing(3, "HOSE-S", "Small hoses"));
    d.Add(Thing(4, "HOSE-L", "Large hoses"));
    d.Add(Thing(5, "BATT-P", "Primary battery (14.5v nominal)"));
    d.Add(Thing(6, "BATT-S", "Secondary batteries (1.5v nominal)"));
    d.Add(Thing(7, "CRKSFT", "Crank shaft"));
    d.Add(Thing(8, "REAC-F", "Fusion reactor power source"));

    cout << *d.FindBy_Desc("Crank shaft") << endl;
    d.Remove(*d.FindBy_Desc("Crank shaft"));
    cout << *d.FindBy_Desc("Crank shaft") << endl;
    return 0;
}

不足之处:

  1. 存储结构是只读的。这是一个必要的缺点,因为如果您在数据存储区中修改对象的索引字段,则索引将变得过时。要修改对象,请将其移除,然后重新添加另一个对象。
  2. 所有字段都必须是唯一的。这很容易更改,但您需要保留包含 list&lt;Thing&gt; 的映射作为非唯一字段的索引,而不仅仅是包含 Thing 的映射。
  3. 与使用std::map 相关的性能问题。 std::unordered_map 是一种替代方案,对于大型数据结构具有更好的(恒定摊销)访问时间(与 std::unordered_set 相同)。

偏差:

  1. 鉴于您在这里有明确的键值关系,我认为您最好使用地图而不是集合。
  2. 为了解决与引用计数相关的性能问题,如果您时刻注意保持内部一致性,您可以放弃所有智能指针以使用原始指针,并通过引用返回值,您可以通过以下方式进一步改进在填充它时使用不安全的对象所有权语义(即,将指针传递给数据存储区随后获得所有权的堆对象)。更复杂,但最终更少的副本和更少的运行时开销。

【讨论】:

  • 感谢 Wug,它又好又干净!我肯定会使用您的对象存储模式。现在我必须决定 - 原始指针或引用计数。 :-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-18
  • 2012-10-05
  • 2012-09-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多