Создание нескольких индексов в большой коллекции объектов с интеллектуальными указателями

Я создаю несколько индексов (то есть, которые используют разные ключи) в большой набор объектов. Объекты могут меняться, и коллекция может сокращаться и расти. Мои мысли до сих пор:

Сохранять несколько наборов указателей на объекты. Используйте вместо использования карту вместо лучшей инкапсуляции. Используйте unordered_set для масштабирования с большими наборами данных. В идеале указатели должны быть в какой-то форме умным указателем.

Я могу начать довольно легко с мастер-коллекции unique_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);

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

UPDATE

Извлеченные уроки:

  • Класс Datastore классный
  • reference_wrappers классные
  • xx_set с элементом "datastore" объекта "ключ" более экономичен по площади, чем xx_map. НО... вы не можете легко использовать unique_ptr как ключ в С++ 11. С++ 14, по-видимому, может улучшить функциональность с помощью std::set<Key>::find. Подробнее см. здесь. Итак, на данный момент хранилище данных, которое управляет распределением ресурсов, кажется, имеет больше смысла здесь, чем попытка принудительно использовать unique_ptr в качестве заданного ключа или увеличение хранилища ключей с картами.
  • Не забудьте заставить значения ключа быть константными для жизни объекта (используйте значения const, указанные в конструкторе)

Ответ 1

Вот один из способов.

std::vector<unique_ptr> для хранения элементов данных (чтобы гарантировать, что адреса не изменяются при изменении размера вектора), а затем контейнеры, содержащие reference_wrappers (ссылки для копирования), чтобы сделать индексы.

компилируемый пример:

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

Ответ 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;
}

Недостатки:

  • Структура хранилища доступна только для чтения. Это необходимый недостаток, потому что индекс устареет, если вы измените индексированные поля объекта, пока он находится в хранилище данных. Чтобы изменить объект, удалите его, а затем повторно добавьте еще один.
  • Все поля должны быть уникальными. Это легко изменить, но вам нужно хранить карты, содержащие list<Thing>, как индексы для неисторических полей, а не только карты, содержащие Thing.
  • Проблемы с производительностью, связанные с использованием std::map. std::unordered_map является альтернативой с лучшими (постоянными амортизированными) временами доступа для огромных структур данных (так же, как std::unordered_set).

Отклонение:

  • Учитывая, что здесь у вас четкое соотношение ключевого слова, я думаю, вам будет лучше с картой, чем с набором.
  • Чтобы решить проблемы производительности, связанные с подсчетом ссылок, если вы всегда стараетесь поддерживать внутреннюю согласованность, вы можете отказаться от всех интеллектуальных указателей для сырых и вернуть значения через ссылки, и вы можете добиться дальнейших используя небезопасную семантику владения объектами при ее заполнении (т.е. передать ее указателям на кучу объектов, которые затем хранилище данных). Более сложное, но в конечном итоге меньшее количество копий и меньшее количество времени выполнения.