【问题标题】:Move out element of std priority_queue in C++11在 C++11 中移出 std priority_queue 的元素
【发布时间】:2013-12-07 14:31:50
【问题描述】:

最小的工作示例。

#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 作为你的答案,但我得到了同样的错误。

【问题讨论】:

  • priority_queue::top() 返回 const 引用,因此即使移动后它仍然是左值。
  • @C.R. std::move 只是对右值引用的强制转换,因此任何const T 类型的左值都将转换为const T&amp;&amp;,这是一个右值——即使您无法通过它获取资源。

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


【解决方案1】:

这似乎是std::priority_queue&lt;T&gt; 设计中的疏忽。似乎没有办法直接将元素移出(而不是复制)。问题是top() 返回一个const T&amp;,所以它不能绑定到T&amp;&amp;。而pop() 返回void,所以你也无法摆脱它。

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

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

正如@DyP 在 cmets 中指出的那样,您应该确保移出的对象仍然可以传递给队列的比较器。而且我相信,为了保留队列的先决条件,它必须像以前一样进行比较(这几乎是不可能实现的)。

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

当然,这样的函数应该有非常好的文档记录。


请注意,每当您为类提供自定义复制/移动构造函数时,您也应该提供相应的复制/移动赋值运算符(否则,该类的行为可能不一致)。因此,只需给您的班级一个已删除的复制赋值运算符和一个适当的移动赋值运算符。

(注意:是的,在某些情况下你想要一个可移动构造但不能移动分配的类,但它们非常罕见(如果你找到它们就会知道它们)。如一个经验法则,总是同时提供 ctor 和 assignment op)

【讨论】:

  • 谢谢,但我仍然遇到错误,因为我的operator= 丢失了(错误来自l.pop() 行)。我们也可以绕过这个错误吗?
  • @Johannes 已编辑;只需提供作业操作。
  • 可能有必要仔细制定对 moved 对象的要求:它仍然必须能够与之进行比较,因为 top+pop 不是“原子的”。也许有人可以提出延期?
  • @DyP 正确,确实是我没有考虑到的。但是(暂时忽略多线程),这会是一个实际问题吗?如果top() 后面紧跟pop(),那么比较器是否会在从顶部移动的项目上调用?
  • isocpp 提案论坛从 5 月开始就有解决这个问题的提案,见groups.google.com/a/isocpp.org/d/msg/std-proposals/TIst1FOdveo/…
【解决方案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】:

    扩展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();
    }
    

    更新。正如@FrançoisAndrieux 指出的那样,在实际代码中最好:避免使用priority_queue_top_and_pop(技术上是UB);从std::priority_queue 私下继承extended_priority_queue 以避免不必要的隐式转换。

    【讨论】:

    • 此解决方案使用户意外使用多态的extended_priority_queue,例如对象切片或拥有错误类型的指针(例如std::unique_ptr&lt;std::priority_queue&lt;T&gt;&gt;拥有extended_priority_queue&lt;T&gt;。它会使其安全如果使用private继承,代价是必须在extended_priority_queue中实现std::priority_queue的接口。
    • @FrançoisAndrieux,不会有这样的切片,因为extended_priority_queue 没有添加新的数据成员。事实上,我在priority_queue_top_and_pop 实现中回复了多态性。两个问题: 1. 您是否发现当前的extended_priority_queue 实现有任何问题,或者您是否担心可能会添加数据成员的未来版本? 2. 如何使用私有继承实现priority_queue_top_and_pop?我认为您需要此功能,因为您不能总是替换底层类型。例如,它可能来自图书馆。
    • 存在未来更改导致切片的风险,以及使用std::priority_queue* 意外删除extended_priority_queue 的直接风险。 private 继承也没有障碍,这不会改变 extended_priority_queue 可以访问的内容。但更重要的是,看看你的辅助函数,static_cast 是未定义的行为,除非PriorityQueueextended_priority_queue。如果它实际上不是派生类型,则不能将基类型的对象强制转换为派生类型之一。
    • 嗯,好点子。你认为有什么方法可以从现有的std::priority_queue 中提取元素吗?可能是,一些reinterpret_cast/memcpy 技巧?
    • 没有 memcpy 或 cast 技巧可以做,继承是正确的解决方案,但应该是 private。标准库为此提供了c 成员,这需要继承。
    【解决方案4】:

    没有非(const-ref)top() 可能有一个很好的理由:修改对象会破坏priority_queue 不变量。所以这个 const_cast 技巧可能只有在你弹出之后才会起作用。

    【讨论】:

      【解决方案5】:

      这里有什么区别?

      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&amp;,所以你不能调用std::move(它需要一个T&amp;&amp;)。

      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&amp;&amp;

      我不确定为什么top 没有非常量重载(可能是标准中的疏忽?)。您可以使用const_cast 来解决这个问题,但请确保您编写完整的 cmets 来描述您正在做什么以及为什么。

      【讨论】:

      • 谢谢,但是l.pop() 行中缺少operator= 问题的任何解决方案?
      • 您需要定义该函数。目前您正在定义移动构造函数,而不是移动赋值运算符。
      • top() 函数没有非常量重载,因为对队列中元素的任何修改都可能破坏队列不变量。请注意,放入优先级队列的项目的 会影响它的顺序,这与仅按顺序存储元素的列表不同。
      【解决方案6】:

      排名靠前的答案看起来不错,但不幸的是,它与-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 &amp;&amp; ./a.out 运行此程序(不是 clang,在这种情况下宏不会执行任何操作),您将在比较器中触发空指针取消引用。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-03-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多