10

I am maintaining a set of unique_ptr instances in a priority_queue. At some point, I want to get the first element and remove it from the queue. However, this always produces a compiler error. See sample code below.

int main ()
{
  std::priority_queue<std::unique_ptr<int>> queue;
  queue.push(std::unique_ptr<int>(new int(42)));

  std::unique_ptr<int> myInt = std::move(queue.top());
  return 1;
}

This produces the following compiler error (gcc 4.8.0):

uptrtest.cpp: In function ‘int main()’: uptrtest.cpp:6:53: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = int; _Dp = std::default_delete<int>]’    std::unique_ptr<int> myInt = std::move(queue.top());
                                                     ^ In file included from /usr/include/c++/4.8/memory:81:0,
                 from uptrtest.cpp:1: /usr/include/c++/4.8/bits/unique_ptr.h:273:7: error: declared here
       unique_ptr(const unique_ptr&) = delete;
       ^

Changing the code to use queue like in this question fixes the issue and the code compiles just fine.

Is there no way to keep unique_ptrs in a priority_queue or am I missing something?

Community
  • 1
  • 1
Chris
  • 6,914
  • 5
  • 54
  • 80

5 Answers5

11

std::priority_queue::top() returns a const reference so you can't move it. Looking at the public interface of priority_queue there is no method to get a non-const reference that you can move (which is mandatory for unique_ptr, it has no copy constructor).

Solution: replace unique_ptr with shared_ptr to be able to copy them (and not just move them).

Or, of course, use another kind of container altogether (but if you chose priority_queue in the first place, this is probably not acceptable for you).

You could also maybe use a "protected member hack" to access the protected member c (the underlying container) but I wouldn't recommend it, this is quite dirty and quite probably UB.

syam
  • 14,701
  • 3
  • 41
  • 65
  • 3
    The practical reason has to do with the difficulty of creating an exception-safe "extract element and modify container" method. So C++ `std` container-editing functions do not extract elements, and element extractors do not modify the container, as a general rule. – Yakk - Adam Nevraumont May 21 '13 at 02:59
  • @Yakk Thanks. Can you also explain why `std::queue` implements a non-const front() method? – Chris May 21 '13 at 03:15
  • 3
    @Chris: I *think* `priority_queue` only provides const access to its elements because otherwise one would be able to break invariants (namely, the elements ordering according to the comparison function). `queue` doesn't have such invariants so it's fine to allow you to modify its elements. – syam May 21 '13 at 03:20
  • 3
    Yep. If you changed the front element of the `priority_queue`, you'd break the ordering requirements of the container. And most `std` containers with such invariants break hard if you violate the invariants: the code tends to be written and use those assumptions to run faster than it could otherwise... – Yakk - Adam Nevraumont May 21 '13 at 03:28
9

I agree, this is incredibly annoying. Why does it let me std::move elements into the queue, then give me no way of moving them out? We no longer have a copy of the original, so I need a non-const object when I do a top() and pop().

Solution: extend std::priority_queue, adding a method pop_top() that does both at once. This should preserve any ordering of the queue. It does depend on c++11 though. The following implementation only works for gcc compilers.

template<typename _Tp, typename _Sequence = std::vector<_Tp>,
    typename _Compare = std::less<typename _Sequence::value_type> >
class priority_queue: std::priority_queue<_Tp, _Sequence, _Compare> {
public:
    typedef typename _Sequence::value_type value_type;
public:

#if __cplusplus < 201103L
explicit
priority_queue(const _Compare& __x = _Compare(),
        const _Sequence& __s = _Sequence()) : 
        std::priority_queue(__x, __s) {}
#else
explicit 
priority_queue(const _Compare& __x, const _Sequence& __s) :
        std::priority_queue<_Tp, _Sequence, _Compare>(__x, __s) {}

explicit 
priority_queue(const _Compare& __x = _Compare(), _Sequence&& __s =
        _Sequence()) :
        std::priority_queue<_Tp, _Sequence, _Compare>(__x, std::move(__s)) {}
#endif

using std::priority_queue<_Tp, _Sequence, _Compare>::empty;
using std::priority_queue<_Tp, _Sequence, _Compare>::size;
using std::priority_queue<_Tp, _Sequence, _Compare>::top;
using std::priority_queue<_Tp, _Sequence, _Compare>::push;
using std::priority_queue<_Tp, _Sequence, _Compare>::pop;

#if __cplusplus >= 201103L

using std::priority_queue<_Tp, _Sequence, _Compare>::emplace;
using std::priority_queue<_Tp, _Sequence, _Compare>::swap;

/**
 *  @brief  Removes and returns the first element.
 */
value_type pop_top() {
    __glibcxx_requires_nonempty();

    // arrange so that back contains desired
    std::pop_heap(this->c.begin(), this->c.end(), this->comp);
    value_type top = std::move(this->c.back());
    this->c.pop_back();
    return top;
}

#endif

};
Antonio Sanchez
  • 346
  • 4
  • 7
  • Shouldn't you also be `std:move`ing for the `return top` line? – Zulan Dec 01 '15 at 09:59
  • I suppose you should... but it's done automatically because you're returning a local variable. For completeness you are correct though. – Antonio Sanchez Nov 05 '16 at 21:40
  • 3
    No you shouldn't. It's always implied and not a useful C++ idiom. – Matthias Urlichs Jan 25 '17 at 14:33
  • This sort of thing is not exception safe - if the return by value causes a copy which throws an exception the value is lost forever. – Chris Hartman Nov 22 '17 at 22:46
  • @ChrisHartman True, but the use-case is for objects that are only move-assignment-able, otherwise we wouldn't have this issue (and you could just use top()). It's your duty to ensure that the move assignment cannot throw an exception. – Antonio Sanchez Nov 28 '17 at 01:10
  • @AntonioSanchez Agreed, I just wanted to make sure the point was clear. I can see an argument for an extension (using SFINAE) to containers to allow "pop_top()" / back_and_pop_back()" / whateverYouWantToCallIt if there are noexept moves. – Chris Hartman Nov 29 '17 at 03:37
0

This is another ugly workaround. Personally, I like it more than the other suggested ugly workarounds. Depending on your situatation, you might need to use this one. Store raw pointers in the priority queue. Every time you pop make sure to put it in a unique pointer. Also have a destructor to clean up remaining stuff.

Here is an untested code.

class Queue_with_extra_steps {
public:
  void push( std::unique_ptr<int>&& value ) {
    queue.push( value.release() );
  }
  std::unique_ptr<int> pop() {
    if( queue.empty() ) {
      return nullptr;
    }
    std::unique_ptr<int> ans( queue.top() );
    queue.pop();
    return ans;
  }
  ~Queue_with_extra_steps() {
    while( !queue.empty() ) {
      this->pop();
    }
  }
  // Add other functions if need be.
private:
  std::priority_queue<int*> queue;
};
Hashimoto
  • 306
  • 4
  • 8
  • From my understanding, this is a very dangerous solution. You push a unique pointer into the priority queue and this just saves the raw pointer in the queue. Afterwards, you pop it and create a new unique pointer. You now have the original unique pointer and the new unique pointer both pointing to the same object. This is undefined behavior! See also https://stackoverflow.com/q/49334967/447490 – Chris Nov 26 '19 at 14:18
  • 4
    @Chris no. release() will empty out the first original unique_ptr (don't mistake it with get()). That is why I am accepting an xvalue for push() to tell the user that the unique_ptr that they are sending in will be diminished. To call push you need to do queue_with_extra_steps.push( std::move(unique_pointer) ), and then the unique_pointer will be empty. – Hashimoto Nov 27 '19 at 02:59
0

A little less ugly workaround is to create wrapper with mutable unique_ptr. You can also separate "priority data" to keep it safe from breaking the heap.

#include <iostream>
#include <queue>
#include <memory>
using namespace std;

template<typename T>
struct unique_ptr_storage{
    unique_ptr_storage(uint32_t _priority,std::unique_ptr<T> &&_ptr)
    : priority(_priority)
    , ptr(std::move(_ptr)){}
    
    mutable std::unique_ptr<T> ptr;
    std::unique_ptr<T> release()const{
        return std::move(ptr);
    }
    uint32_t priority;
    bool operator<(const unique_ptr_storage<T> &other)const{
        return priority<other.priority;
    }
};

int main() {
    std::priority_queue<unique_ptr_storage<int>> q;
    
    q.emplace(1,std::make_unique<int>(10));
    q.emplace(3,std::make_unique<int>(30));
    q.emplace(2,std::make_unique<int>(20));
    
    while(!q.empty()){
        std::unique_ptr<int> p=q.top().release();
        q.pop();
        std::cout<<*p<<std::endl;
    }
    return 0;
}

Output:

30
20
10
Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
modimo
  • 11
  • 1
0

Since std::priority_queue::top() returns a const reference, we have to use some alternative approach for addressing it. Depending on your objects' life time, you may choose this solution:

  1. store a const MyObject * as the value in the priority_queue
  2. std::priority_queue::top() will give you an const MyObject *
  3. use the const_cast (https://en.cppreference.com/w/cpp/language/const_cast) to drop the const qualifier from the pointer

With this solution, if you need to manage the objects' life time as well, you may manage the unique_ptr outside of priority_queue in another container while still leveraing priority_queue's capability for ordering your objects.

nybon
  • 8,894
  • 9
  • 59
  • 67