Поиск необработанного указателя для наборов 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. Полагаю, вам лучше с общим указателем в этом случае. Просто храните общие указатели и передавайте их, так как кто-то рядом с этим набором явно сохраняет их.