unique_ptrs集的原始指针查找

Raw pointer lookup for sets of unique_ptrs

本文关键字:原始 指针 查找 ptrs unique      更新时间:2023-10-16

我经常发现自己想写这样的代码:

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 参数(我显然没有,因为它们归集合本身所有)。

我能想到两个主要的解决方案。

  1. 创建用于查找的临时unique_ptr。例如,上面的 removeObject() 可以像这样实现:

    void MyClass::removeObject(const Object* target)
    {
      std::unique_ptr<Object> targetSmartPtr(target);
      objects.erase(targetSmartPtr);
      targetSmartPtr.release();
    }
    
  2. 将该集替换为指向unique_ptrs的原始指针的映射。

      // ...
      std::map<const Object*, std::unique_ptr<Object>> objects;
    };
    

然而,在我看来,两者都有点愚蠢。在解决方案 1 中,erase() 不是 noexcept,因此临时unique_ptr可能会删除它并不真正拥有的对象,而 2 不必要地需要容器的双倍存储空间。

我知道 Boost 的指针容器,但与现代 C++11 标准库容器相比,它们当前的功能有限。

我最近阅读了大约 C++14 的内容,并遇到了"向关联容器添加异构比较查找"。但根据我对它的理解,查找类型必须与键类型相当,但原始指针无法与unique_ptrs相媲美。

有谁知道一个更优雅的解决方案或即将添加到C++

来解决这个问题?

> 在 C++14 中,如果Compare::is_transparent存在,std::set<Key>::find是一个template函数。 你传入的类型不需要是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类)并找到正确的元素。

在第 C++14 之外,您被迫使用map unique_ptr方法或等效方法,因为find的签名过于严格。 或者编写自己的等效set

另一种可能性,接近公认的答案,但略有不同和简化。

我们可以利用标准比较器std::less<>(没有模板参数)是透明的这一事实。然后,我们可以在全局命名空间中提供我们自己的比较函数:

// These two are enough to be able to call objects.find(raw_ptr)
bool operator<(const unique_ptr<Object>& lhs, const Object* rhs) {
  return std::less<const Object*>()(lhs.get(), rhs);
}
bool operator<(const Object* lhs, const unique_ptr<Object>& rhs) {
  return std::less<const Object*>()(lhs, rhs.get());
}
class MyClass
{
  // ...
private:
  std::set<std::unique_ptr<Object>, std::less<>> objects;  // Note std::less<> here
};

您可以尝试使用 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;

有关详细信息,请参阅提升多索引容器文档

或者你可以到处使用 std::shared_ptr,或者在 set 中使用原始指针?

为什么需要通过原始品特查找?如果您将其存储在任何位置并使用此指针检查该对象是否有效,则最好使用 std::shared_ptr 存储在容器中,使用 std::weak_ptr 存储其他对象。在这种情况下,在使用之前,您根本不需要通过原始指针进行查找。

虽然绝对是一个黑客,但我刚刚意识到可以用新的放置来构建一个临时的"愚蠢"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_setstd::map 键和std::unordered_map键,所有这些都仅使用标准 C++11,几乎零不必要的开销。

更新 2:Yakk 是正确的,没有办法在没有重大妥协的情况下使用标准 C++11 容器做到这一点。在最坏的情况下,某些内容将以线性时间运行,或者您在问题中编写了这些解决方法。

我会考虑两种解决方法。

我会尝试排序std::vector,类似于 boost::container::flat_set。是的,在最坏的情况下,插入/擦除将是线性时间。不过,它可能比您想象的要快得多:与基于节点的容器(例如std::set)相比,连续容器对缓存非常友好。请阅读他们在boost::container::flat_set上写的内容。这种妥协对你来说是否可以接受,我无法判断/衡量。

其他人也提到了std::share_ptr。我个人尽量避免它们,主要是因为"共享指针和全局变量一样好"(Sean Parent)。我不使用它们的另一个原因是因为它们重量很重,部分原因是我通常不需要的所有多线程东西。但是,定义BOOST_SP_DISABLE_THREADS时,boost::shared_ptr 会消除与多线程相关的所有开销。我相信在您的情况下,使用boost::shared_ptr将是最简单的解决方案。


更新:正如Yakk善意指出的那样,我的方法具有线性时间复杂度... :(



(第一个版本。

您可以通过将自定义比较器传递给 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();
}

您在这里使用独特的品画家。这意味着,您的集合具有对象的唯一所有权。现在,这应该意味着,如果对象确实存在,它要么在集合中,要么你有一个唯一的指针。在这种情况下,您甚至不需要查找集合。

但对我来说,情况似乎并非如此。我想在这种情况下,你最好使用共享指针。只需存储共享指针并传递它们,因为此集合旁边的人清楚地存储它们。