【问题标题】:Raw pointer lookup for sets of unique_ptrs对 unique_ptrs 集的原始指针查找
【发布时间】:2013-09-27 04:56:46
【问题描述】:

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

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++ 解决方案?

【问题讨论】:

  • 有趣,这看起来像是容器类中的设计疏忽——应该有一种简单的方法来做到这一点。
  • 同意。也许应该有类似于 Yakk 的 pointer_comp 的 std::ptr_lessstd::ptr_equal_tostd::ptr_hash 等,以简化指针比较/查找。它们与 C++1y 对关联容器的异构比较查找配合得很好。
  • 离题:您不应该通过右值引用获取唯一指针;仅按价值就足够了。

标签: c++ c++11 unique-ptr c++14


【解决方案1】:

C++14 中,如果Compare::is_transparent 存在,则std::set&lt;Key&gt;::findtemplate 函数。你传入的类型不需要是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&lt;Foo&gt;Foo*shared_ptr&lt;Foo&gt;(或Foo 的任何派生类)并找到正确的元素。

在 C++14 之外,您不得不使用 mapunique_ptr 的方法,或等效的方法,因为 find 的签名过于严格。或编写您自己的 set 等效项。

【讨论】:

  • @ali std::lower_boundset 迭代器上是线性的。没有随机访问。
  • +1。显然,标准 C++11 容器无法做到这一点而不做出重大妥协。在最坏的情况下,要么某些东西会在线性时间内运行,要么有 OP 中提到的那些变通方法。
  • @JoBates transparent 哈希函数可能比 transparent 比较器稍微复杂一点,但在某些情况下很有用:与 const char* 缓冲区或 @987654343 一起使用的 std::string 哈希@ 可以在查找时保存分配。并且“只有钥匙的一部分实际上是钥匙”的事情也适用。
  • 很好的答案!仍在尝试完全掌握 C++11,但似乎已经有一个令人信服的理由过渡到 C++14。奇怪的是,unique_ptr + stl 容器到处都被誉为伟大的东西。没有发现任何提及这个问题,我认为这是一个主要缺点。
  • @Yakk,我实际上想要一个unique_ptrs 的unordered_set,因为我需要快速查找(不是按索引)并且容器经常更改。目前我使用unordered_map,因为它允许按键擦除。这里真正的“问题”是:对于更改某些包含unique_ptrs 的私有容器的remove(xxx) 方法,什么是正确的参数?
【解决方案2】:

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

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

// 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
};

【讨论】:

    【解决方案3】:

    您可以尝试将 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 documentation

    或者您可以在任何地方使用 std::shared_ptr,或者在 set 中使用原始指针?

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

    【讨论】:

    • 我的印象是你不能拥有 unique_ptr 的 multi_index_container,因为它们被复制了。
    【解决方案4】:

    虽然绝对是一个 hack,但我刚刚意识到可以构建一个临时的“哑”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::mapstd::unordered_map 键,它们都仅使用标准 C++11,几乎没有不必要的开销。

    【讨论】:

    • 为了更正确,您需要使用 alignas 来确保数组与 unique_ptr 对象适当对齐。另一种选择只是unique_ptr&lt;Object&gt; key(target); objects.erase(key); key.release(); ...尽管如果Object 析构函数可以抛出(无论如何这会很糟糕),您将获得双重删除,因此需要处理来自erase 调用的异常。
    【解决方案5】:

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

    我会考虑两种解决方法。

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

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


    更新:作为Yakk kindly pointed out,我的方法具有线性时间复杂度... :(



    (第一个版本。)

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

    【讨论】:

      【解决方案6】:

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

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

      【讨论】:

      • 不,一点也不。 unique_ptr 是关于所有权。这并不意味着没有其他指向该对象的指针,只是没有其他所有者。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-04-25
      • 2011-09-07
      • 1970-01-01
      • 2014-11-10
      • 2021-11-15
      相关资源
      最近更新 更多