Создание нескольких индексов в большой коллекции объектов с интеллектуальными указателями
Я создаю несколько индексов (то есть, которые используют разные ключи) в большой набор объектов. Объекты могут меняться, и коллекция может сокращаться и расти. Мои мысли до сих пор:
Сохранять несколько наборов указателей на объекты.
Используйте вместо использования карту вместо лучшей инкапсуляции.
Используйте 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
).
Отклонение:
- Учитывая, что здесь у вас четкое соотношение ключевого слова, я думаю, вам будет лучше с картой, чем с набором.
- Чтобы решить проблемы производительности, связанные с подсчетом ссылок, если вы всегда стараетесь поддерживать внутреннюю согласованность, вы можете отказаться от всех интеллектуальных указателей для сырых и вернуть значения через ссылки, и вы можете добиться дальнейших используя небезопасную семантику владения объектами при ее заполнении (т.е. передать ее указателям на кучу объектов, которые затем хранилище данных). Более сложное, но в конечном итоге меньшее количество копий и меньшее количество времени выполнения.