Пользовательские распределители как альтернатива вектору умных указателей?
Этот вопрос касается владения указателями, их использования, умных указателей, векторов и распределителей.
Я немного заблудился в своих мыслях об архитектуре кода. Кроме того, если на этот вопрос уже есть где-то ответ, 1. извините, но я до сих пор не нашел удовлетворительного ответа и 2. пожалуйста, укажите мне на него.
Моя проблема заключается в следующем:
У меня есть несколько "вещей", хранящихся в векторе, и несколько "потребителей" этих "вещей". Итак, моя первая попытка была такой:
std::vector<thing> i_am_the_owner_of_things;
thing* get_thing_for_consumer() {
// some thing-selection logic
return &i_am_the_owner_of_things[5]; // 5 is just an example
}
...
// somewhere else in the code:
class consumer {
consumer() {
m_thing = get_thing_for_consumer();
}
thing* m_thing;
};
В моем приложении это было бы безопасно, потому что "вещи" переживают "потребителей" в любом случае. Однако во время выполнения можно добавить больше "вещей", и это может стать проблемой, потому что если std::vector<thing> i_am_the_owner_of_things;
перераспределяется, все указатели thing* m_thing
становятся недействительными.
Исправление в этом сценарии состояло бы в том, чтобы хранить уникальные указатели на "вещи" вместо "вещей" напрямую, то есть следующим образом:
std::vector<std::unique_ptr<thing>> i_am_the_owner_of_things;
thing* get_thing_for_consumer() {
// some thing-selection logic
return i_am_the_owner_of_things[5].get(); // 5 is just an example
}
...
// somewhere else in the code:
class consumer {
consumer() {
m_thing = get_thing_for_consumer();
}
thing* m_thing;
};
Недостатком здесь является то, что когерентность памяти между "вещами" теряется. Может ли эта когерентность памяти быть восстановлена с помощью пользовательских распределителей как-то? Я имею в виду нечто вроде распределителя, который всегда выделял бы память, например, для 10 элементов за раз, и всякий раз, когда требовалось, добавлял больше кусков памяти размером 10 элементов.
Пример:
первоначально:
v = ☐☐☐☐☐☐☐☐☐☐
больше элементов:
v = ☐☐☐☐☐☐☐☐☐☐ 🡒 ☐☐☐☐☐☐☐☐☐☐
и опять:
v = ☐☐☐☐☐☐☐☐☐☐ 🡒 ☐☐☐☐☐☐☐☐☐☐ 🡒 ☐☐☐☐☐☐☐☐☐☐
Используя такой распределитель, мне даже не пришлось бы использовать std::unique_ptr
для "вещей", потому что во время перераспределения std::vector
адреса памяти уже существующих элементов не изменились бы.
В качестве альтернативы я могу думать только о том, чтобы ссылаться на "вещь" в "потребителе" через std::shared_ptr<thing> m_thing
, в отличие от текущей thing* m_thing
но это кажется мне худшим подходом, потому что "вещь "не должен владеть" потребителем ", и с помощью общих указателей я бы создал совместное владение.
Итак, подход распределителя хорош? И если так, как это можно сделать? Должен ли я сам применять распределитель или он существует?
Ответы
Ответ 1
Если вы можете рассматривать thing
как тип значения, сделайте это. Это упрощает вещи, вам не нужен умный указатель для обхода проблемы аннулирования указателя/ссылки. Последний может быть решен по-разному:
- Если новые
thing
экземпляры вставляются через push_front
и push_back
во время выполнения программы, используйте std::deque
вместо std::vector
. Тогда никакие указатели или ссылки на элементы в этом контейнере не будут признаны недействительными (хотя итераторы недействительны - спасибо @odyss-jii за указание на это). Если вы боитесь, что вы сильно полагаетесь на выигрыш в производительности от полностью непрерывного макета памяти std::vector
: создайте тест и профиль. - Если новые экземпляры
thing
вставляются в середину контейнера во время выполнения программы, попробуйте использовать std::list
. Никакие указатели/итераторы/ссылки недействительны при вставке или удалении элементов контейнера. Итерация по std::list
намного медленнее, чем std::vector
, но убедитесь, что это актуальная проблема в вашем сценарии, прежде чем беспокоиться об этом.
Ответ 2
На этот вопрос нет однозначного правильного ответа, поскольку он во многом зависит от точных шаблонов доступа и желаемых характеристик производительности.
Сказав это, вот моя рекомендация:
Продолжайте хранить данные непрерывно, как и вы, но не храните указатели псевдонимов для этих данных. Вместо этого рассмотрим более безопасную альтернативу (это проверенный метод), когда вы выбираете указатель на основе идентификатора непосредственно перед его использованием - как примечание: в многопоточном приложении вы можете заблокировать попытки изменить размер основного хранилища, пока такая слабая ссылка живет.
Таким образом, ваш потребитель будет хранить идентификатор и извлекать указатель на данные из "хранилища" по запросу. Это также дает вам контроль над всеми "выборками", так что вы можете отслеживать их, применять меры безопасности и т.д.
void consumer::foo() {
thing *t = m_thing_store.get(m_thing_id);
if (t) {
// do something with t
}
}
Или более продвинутая альтернатива, чтобы помочь с синхронизацией в многопоточном сценарии:
void consumer::foo() {
reference<thing> t = m_thing_store.get(m_thing_id);
if (!t.empty()) {
// do something with t
}
}
Где reference
будет каким-то потокобезопасным RAII "слабый указатель".
Есть несколько способов реализации этого. Вы можете использовать хеш-таблицу с открытой адресацией и использовать идентификатор в качестве ключа; это даст вам примерно O (1) время доступа, если вы правильно его уравновесите.
Другая альтернатива (O (1) в лучшем случае, O (N) в худшем случае) - использовать "опорную" структуру с 32-битным идентификатором и 32-битным индексом (такой же размер, как у 64-битного указателя) - индекс служит своего рода кешем. Когда вы выбираете, вы сначала пробуете индекс, если элемент в индексе имеет ожидаемый идентификатор, который вы сделали. В противном случае вы получаете "промах кэша" и выполняете линейное сканирование магазина, чтобы найти элемент на основе идентификатора, а затем сохраняете последнее известное значение индекса в вашей ссылке.
Ответ 3
[Общий указатель] кажется мне худшим подходом, потому что "вещь" не должна владеть "потребителем", и с помощью общих указателей я бы создал общее владение.
И что? Может быть, код немного менее самодокументируется, но он решит все ваши проблемы. (И, между прочим, вы путаете вещи, используя слово "потребитель", которое в традиционной парадигме производитель/потребитель получит право собственности.)
Кроме того, возвращение необработанного указателя в вашем текущем коде уже совершенно неоднозначно в отношении владения. В целом, я бы сказал, что это хорошая практика - избегать необработанных указателей, если вы можете (например, вам не нужно вызывать delete
.) Я бы возвратил ссылку, если вы используете unique_ptr
std::vector<std::unique_ptr<thing>> i_am_the_owner_of_things;
thing& get_thing_for_consumer() {
// some thing-selection logic
return *i_am_the_owner_of_things[5]; // 5 is just an example
}
Ответ 4
ИМО лучшим подходом будет создать новый контейнер, который будет вести себя безопасным способом.
Плюсы:
- изменение будет сделано на отдельном уровне абстракции
- изменения в старом коде будут минимальными (просто замените
std::vector
новым контейнером). - это будет "чистый код" способ сделать это
Минусы:
- может показаться, что работы еще немного
Другой ответ предлагает использовать std::list
который будет выполнять эту работу, но с большим количеством выделения и более медленным произвольным доступом. Так что IMO лучше составить собственный контейнер из пары std::vector
s.
Так что это может начать выглядеть более или менее так (минимальный пример):
template<typename T>
class cluster_vector
{
public:
static const constexpr cluster_size = 16;
cluster_vector() {
clusters.reserve(1024);
add_cluster();
}
...
size_t size() const {
if (clusters.empty()) return 0;
return (clusters.size() - 1) * cluster_size + clusters.back().size();
}
T& operator[](size_t index) {
thowIfIndexToBig(index);
return clusters[index / cluster_size][index % cluster_size];
}
void push_back(T&& x) {
if_last_is_full_add_cluster();
clusters.back().push_back(std::forward<T>(x));
}
private:
void thowIfIndexToBig(size_t index) const {
if (index >= size()) {
throw std::out_of_range("cluster_vector out of range");
}
}
void add_cluster() {
clusters.push_back({});
clusters.back().reserve(cluster_size);
}
void if_last_is_full_add_cluster() {
if (clusters.back().size() == cluster_size) {
add_cluster();
}
}
private:
std::vector<std::vector<T>> clusters;
}
Таким образом, вы предоставите контейнер, который не будет перераспределять предметы. Он не измеряет то, что делает Т.