在C ++ 11中移出std priority_queue的元素

时间:2013-11-22 16:14:33

标签: c++ c++11 move-semantics move-constructor

最小的工作示例。

#include <cassert>
#include <list>
#include <queue>
//#define USE_PQ

struct MyClass
{
    const char* str;
    MyClass(const char* _str) : str(_str) {}
    MyClass(MyClass&& src) { str = src.str; src.str = nullptr; }
    MyClass(const MyClass&) = delete;
};

struct cmp_func
{
    bool operator() (const MyClass&, const MyClass&) const
    {
        return true;
    }
};

typedef std::priority_queue<MyClass, std::vector<MyClass>, cmp_func> pq_type;

#ifdef USE_PQ
MyClass remove_front(pq_type& l)
{
    MyClass moved = std::move(l.top());
    // error from the above line:
    // use of deleted function ‘MyClass::MyClass(const MyClass&)’
    l.pop();
    return std::move(moved);
}
#else
MyClass remove_front(std::list<MyClass>& l)
{
    MyClass moved = std::move(l.front());
    l.erase(l.begin());
    return std::move(moved);
}
#endif

int main()
{
    const char* hello_str = "Hello World!";
    MyClass first(hello_str);
#ifdef USE_PQ
    pq_type l;
    l.push(std::move(first));
    MyClass moved = remove_front(l);
#else
    std::list<MyClass> l;
    l.push_back(std::move(first));
    MyClass moved = remove_front(l);
#endif
    assert(moved.str);
    assert(!first.str);
    return 0;
}

所以这很有效。现在从第4行删除注释符号,它表示需要复制构造函数(我的删除)。此外,它错过了operator=。问题:

  • 这里有什么区别?
  • 问题可以解决吗?如果是的话,如果不是,为什么不呢?

注意:您也可以使用boost的priority_queue作为答案,但我也遇到了同样的错误。

6 个答案:

答案 0 :(得分:15)

这似乎是对std::priority_queue<T>设计的疏忽。似乎没有办法直接移动(不复制)元素。问题是top()返回const T&,因此无法绑定到T&&。并且pop()会返回void,因此您也无法将其从中获取。

然而,有一个解决方法。确保优先级队列中的对象实际上不是const就可以了。它们是普通对象,队列只是不提供对它们的可变访问。因此,这样做是完全合法的:

MyClass moved = std::move(const_cast<MyClass&>(l.top()));
l.pop();

正如@DyP在评论中指出的那样,你应该确保移动的对象仍然可以传递给队列的比较器。而且我认为,为了保留队列的前提条件,它必须与以前相比(它几乎不可能实现)。

因此,您应该在函数中封装cast & top()pop()调用,并确保不会对队列进行任何修改。如果这样做,您可以合理地确定不会在移动的对象上调用比较器。

当然,这样的功能应该有很好的记录。


请注意,无论何时为类提供自定义复制/移动构造函数,都应提供相应的复制/移动赋值运算符(否则,类可能表现不一致)。因此,只需为您的类提供一个已删除的复制赋值运算符和一个适当的移动赋值运算符。

(注意:是的,有些情况下你想要一个可移动构造但不能移动分配的类,但它们非常罕见(如果你找到它们,你会知道它们)。根据经验,总是同时提供ctor和赋值操作

答案 1 :(得分:5)

为什么没有非(const-ref)top()可能有一个很好的理由:修改对象会破坏priority_queue不变量。因此,如果你刚刚弹出,const_cast技巧可能只会起作用。

答案 2 :(得分:2)

根据您想要存储在优先级队列中的类型,Angew的解决方案的替代方案,即避免const_cast并且消除了在脚中射击自己的一些机会,将包裹元素类型如下:

struct Item {
    mutable MyClass element;
    int priority; // Could be any less-than-comparable type.

    // Must not touch "element".
    bool operator<(const Item& i) const { return priority < i.priority; }
};

然后将元素移出队列:

MyClass moved = std::move(l.top().element);
l.pop();

这样,对MyClass的移动语义没有特殊要求来保留无效对象上的顺序关系,并且没有优先级队列的不变量被无效的代码部分。

答案 3 :(得分:1)

  

这有什么区别?

MyClass remove_front(pq_type& l)
{
    MyClass moved = std::move(l.top()); // PROBLEM
    l.pop();
    return std::move(moved);
}

std::priority_queue::top会返回const value_type&,因此您无法拨打std::move(需要T&&)。

MyClass remove_front(std::list<MyClass>& l)
{
    MyClass moved = std::move(l.front());
    l.erase(l.begin());
    return std::move(moved);
}

std::list::front有一个返回引用的重载,因此它可以绑定到T&&

我不确定为什么top没有非常量超载(可能是标准中的疏忽?)。您可以使用const_cast来解决这个问题,但请务必撰写完整的评论,说明您在做什么以及为什么。

答案 4 :(得分:1)

扩展std::priority_queue很容易,因为它将基础容器公开为受保护的成员:

template <
    class T,
    class Container = std::vector<T>,
    class Compare = std::less<typename Container::value_type>>
class extended_priority_queue : public std::priority_queue<T, Container, Compare> {
public:
  T top_and_pop() {
    std::pop_heap(c.begin(), c.end(), comp);
    T value = std::move(c.back());
    c.pop_back();
    return value;
  }

protected:
  using std::priority_queue<T, Container, Compare>::c;
  using std::priority_queue<T, Container, Compare>::comp;
};

如果您需要将元素移出std::priority_queue实例,则可以使用extended_priority_queue来实现一个辅助函数:

template<typename PriorityQueue>
auto priority_queue_top_and_pop(PriorityQueue& queue) ->
    typename PriorityQueue::value_type {
  return static_cast<extended_priority_queue<
      typename PriorityQueue::value_type,
      typename PriorityQueue::container_type,
      typename PriorityQueue::value_compare>&>(queue).top_and_pop();
}

答案 5 :(得分:0)

排名靠前的答案看起来不错,但不幸的是,它与-D_GLIBCXX_DEBUG不兼容。例如:

#include <iostream>
#include <memory>
#include <queue>
#include <vector>

struct T {
  int x;
  std::shared_ptr<int> ptr;
  T(int x, std::shared_ptr<int> ptr) : x(x), ptr(ptr) {}
};
struct Compare {
  bool operator()(const T& x, const T& y) {
    return *x.ptr < *y.ptr;
  }
};
int main() {
  auto ptr1 = std::make_shared<int>(3);
  auto ptr2 = std::make_shared<int>(3);
  std::priority_queue<T, std::vector<T>, Compare> f;
  f.emplace(3, ptr1);
  f.emplace(4, ptr2);
  T moved = std::move(const_cast<T&>(f.top()));
  f.pop();
  std::cerr << moved.x << "\n";
}

如果你在GCC上使用g++ foo.cpp -D_GLIBCXX_DEBUG -O0 -g -std=c++11 && ./a.out运行它(不是clang,宏在这种情况下不会做任何事情)你将在比较器中触发空指针解除引用。