Поиск необработанного указателя для наборов unique_ptrs
Я часто нахожу, что хочу написать такой код:
class MyClass
{
public:
void addObject(std::unique_ptr<Object>&& newObject);
void removeObject(const Object* target);
private:
std::set<std::unique_ptr<Object>> objects;
};
Однако большая часть интерфейса std:: set выглядит бесполезной с помощью std:: unique_ptrs, так как для функций поиска требуются параметры std:: unique_ptr (которых я, очевидно, не имею, потому что они принадлежат самому набору).
Я могу думать о двух основных решениях этого.
-
Создайте временный уникальный_ptr для поиска. Например, вышеупомянутый removeObject() может быть реализован следующим образом:
void MyClass::removeObject(const Object* target)
{
std::unique_ptr<Object> targetSmartPtr(target);
objects.erase(targetSmartPtr);
targetSmartPtr.release();
}
-
Замените набор на карту исходных указателей на unique_ptrs.
// ...
std::map<const Object*, std::unique_ptr<Object>> objects;
};
Однако, оба кажутся мне немного глупыми. В решении 1 erase() не является исключением, поэтому временный unique_ptr может удалить объект, который он действительно не имеет, и 2 требует ненужного хранения для контейнера.
Я знаю о контейнерах указателей Boost, но их текущие функции ограничены по сравнению с современными стандартными библиотечными контейнерами С++ 11.
Недавно я читал о С++ 14 и наткнулся на "Добавление гетерогенного поиска сравнений в ассоциативные контейнеры". Но формируйте мое понимание этого, типы поиска должны быть сопоставимы с типами ключей, но исходные указатели не сопоставимы с unique_ptrs.
Кто-нибудь знает более элегантное решение или предстоящее дополнение к С++, которое решает эту проблему?
Ответы
Ответ 1
В С++ 14, std::set<Key>::find
- это функция template
, если существует Compare::is_transparent
. Тип, который вы передаете, не должен быть Key
, просто эквивалентен вашему компаратору.
Итак, напишите компаратор:
template<class T>
struct pointer_comp {
typedef std::true_type is_transparent;
// helper does some magic in order to reduce the number of
// pairs of types we need to know how to compare: it turns
// everything into a pointer, and then uses `std::less<T*>`
// to do the comparison:
struct helper {
T* ptr;
helper():ptr(nullptr) {}
helper(helper const&) = default;
helper(T* p):ptr(p) {}
template<class U, class...Ts>
helper( std::shared_ptr<U,Ts...> const& sp ):ptr(sp.get()) {}
template<class U, class...Ts>
helper( std::unique_ptr<U, Ts...> const& up ):ptr(up.get()) {}
// && optional: enforces rvalue use only
bool operator<( helper o ) const {
return std::less<T*>()( ptr, o.ptr );
}
};
// without helper, we would need 2^n different overloads, where
// n is the number of types we want to support (so, 8 with
// raw pointers, unique pointers, and shared pointers). That
// seems silly:
// && helps enforce rvalue use only
bool operator()( helper const&& lhs, helper const&& rhs ) const {
return lhs < rhs;
}
};
затем используйте его:
typedef std::set< std::unique_ptr<Foo>, pointer_comp<Foo> > owning_foo_set;
теперь owning_foo_set::find
примет unique_ptr<Foo>
или Foo*
или shared_ptr<Foo>
(или любой производный класс Foo
) и найдет правильный элемент.
Вне С++ 14 вы вынуждены использовать подход map
to unique_ptr
или что-то подобное, поскольку подпись find
является чрезмерно ограничительной. Или напишите свой собственный эквивалент set
.
Ответ 2
Вы можете попытаться использовать boost:: multi_index_container с дополнительной индексацией Object *.
Что-то вроде этого:
typedef std::unique_ptr<Object> Ptr;
typedef multi_index_container<
Ptr,
indexed_by<
hashed_unique<Ptr>,
ordered_unique<const_mem_fun<Ptr,Object*,&Ptr::get> >
>
> Objects;
Для получения дополнительной информации см. Документация Boost Multi-Index Containers
Или, может быть, вы можете использовать std:: shared_ptr всюду или вместо этого использовать необработанные указатели в наборе?
Почему вам нужно искать с помощью raw pinter? Если вы храните его где угодно и проверяете, что объект с этим указателем действителен, лучше использовать std:: shared_ptr для хранения в контейнере и std:: weak_ptr для других объектов. В этом случае перед использованием вам не нужен поиск по необработанному указателю.
Ответ 3
Хотя определенно взломал, я просто понял, что можно построить временный "тупой" unique_ptr с новым размещением, а не с отменой риска. removeObject()
может быть написано примерно так:
void MyClass::removeObject(const Object* target)
{
alignas(std::unique_ptr<Object>)
char dumbPtrData[sizeof(std::unique_ptr<Object>)];
objects.erase(
*::new (dumbPtrData) std::unique_ptr<Object>(const_cast<Object *>(target)));
}
Это решение будет работать с ключами std::unordered_set
, std::map
и std::unordered_map
, все с использованием только стандартного С++ 11, с практически нулевыми ненужными служебными данными.
Ответ 4
UPDATE 2: Якк правильный, нет никакого способа сделать это со стандартными контейнерами С++ 11 без значительных компромиссов. Либо что-то будет работать в линейном времени в худшем случае, либо есть такие обходные пути, которые вы пишете в своем вопросе.
Есть два обходных решения, которые я бы рассмотрел.
Я попробовал отсортированный std::vector
, аналогично boost:: container:: flat_set. Да, в худшем случае вставки/стирания будут линейными. Тем не менее, это может быть намного быстрее, чем вы, вероятно, думаете: Контейнерные контейнеры очень удобны для кеширования по сравнению с контейнерами на основе node, такими как std::set
. Пожалуйста, прочитайте, что они пишут в boost:: container:: flat_set. Является ли этот компромисс приемлемым для вас, я не могу сказать/измерить.
Другие также упоминали std::share_ptr
. Я лично стараюсь избегать их, главным образом потому, что "общий указатель не хуже глобальной переменной" (Sean Parent). Еще одна причина, по которой я их не использую, заключается в том, что они имеют большой вес, отчасти из-за всего многопоточного материала, который мне обычно не нужен. Однако boost::shared_ptr
, когда BOOST_SP_DISABLE_THREADS
определен, удаляет все эти служебные данные, связанные с многопоточными. Я считаю, что использование boost::shared_ptr
было бы самым простым решением в вашем случае.
ОБНОВЛЕНИЕ: Как Якк любезно отметил, мой подход имеет линейную временную сложность...:(
(Первая версия.)
Вы можете сделать это, передав произвольный компаратор std::lower_bound()
. Вот рудиментарная реализация:
#include <algorithm>
#include <cassert>
#include <iostream>
#include <memory>
#include <set>
#include <string>
using namespace std;
template <typename T>
class Set {
private:
struct custom_comparator {
bool operator()(const unique_ptr<T>& a, const T* const & b){
return a.get() < b;
}
} cmp;
set<unique_ptr<T>> objects; // decltype at begin() and end()
// needs objects to be declared here
public:
auto begin() const -> decltype(objects.begin()) { return objects.begin(); }
auto end() const -> decltype(objects.end() ) { return objects.end(); }
void addObject(unique_ptr<T>&& newObject) {
objects.insert(move(newObject));
}
void removeObject(const T* target) {
auto pos = lower_bound(objects.begin(), objects.end(), target, cmp);
assert (pos!=objects.end()); // What to do if not found?
objects.erase(pos);
}
};
void test() {
typedef string T;
Set<T> mySet;
unique_ptr<T> a{new T("a")};
unique_ptr<T> b{new T("b")};
unique_ptr<T> c{new T("c")};
T* b_ptr = b.get();
mySet.addObject(move(a));
mySet.addObject(move(b));
mySet.addObject(move(c));
cout << "The set now contains: " << endl;
for (const auto& s_ptr : mySet) {
cout << *s_ptr << endl;
}
mySet.removeObject(b_ptr);
cout << "After erasing b by the pointer to it:" << endl;
for (const auto& s_ptr : mySet) {
cout << *s_ptr << endl;
}
}
int main() {
test();
}
Ответ 5
Здесь вы используете уникальные пинтеры. Это означает, что ваш набор имеет уникальное право собственности на объекты. Теперь это должно означать, что если объект существует, он либо в наборе, либо у вас есть уникальный указатель. В этом случае вам даже не нужно искать набор.
Но мне кажется, что это не hte case. Полагаю, вам лучше с общим указателем в этом случае. Просто храните общие указатели и передавайте их, так как кто-то рядом с этим набором явно сохраняет их.