5
votes

Simplified code:

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

class Foo {
public:
    Foo() {};
    virtual ~Foo() {}
};

int main()
{
    std::queue<std::unique_ptr<Foo>> queue;
    auto element = std::make_unique<Foo>();
    queue.push(std::move(element));
    std::vector<std::queue<std::unique_ptr<Foo>>> vector;
    // Error 1
    vector.push_back(queue); 
    // Error 2
    vector.push_back(std::move(queue));
    // Error 3
    vector.push_back({});
    return 0;
}

Error:

'std::unique_ptr>::unique_ptr(const std::unique_ptr<_Ty,std::default_delete<_Ty>> &)': attempting to reference a deleted function

Obviously copying c~tor of unique_ptr is removed but I'm not trying to copy it. Am I?

1
Yes, push_back makes a copy. To create an object in place, use emplace_back.Ben Voigt
It works if std::queue is replaced by std::vector; doesn't work with the default queue underlier of std::deque. But it does work fine to move a deque that isn't in a vector. WeirdM.M
@M.M Yes, you can move a deque that isn't in a vector. The operation will not be noexcept however. vector just doesn't like potentially throwing moves (see aschepler's answer for details).Arne Vogel

1 Answers

3
votes

This is a bit tricky. All std::vector<T> functions that can increase the size of the vector have to do it in an exception-safe way if either of these two things are true:

  • T has a move constructor that guarantees it will never throw any exceptions; or,

  • T has a copy constructor.

So in most implementations, if T has a move constructor declared nothrow or equivalent, vector will use the move constructor of T for those operations. If not, and T has a copy constructor, vector will use the copy constructor, even if T has a move constructor.

And the problem here is that std::queue always declares it has a copy constructor, even if that copy constructor can't actually be instantiated, and always declares it has a move constructor that might throw, even if the container member's move constructor guarantees it won't throw.

The Standard specifies these in [queue.defn] as:

namespace std {
  template<class T, class Container = deque<T>>
  class queue {
    // ...
  public:
    explicit queue(const Container&);
    explicit queue(Container&& = Container());
    // ...
  };
}

This class template definition could be improved in a couple of ways to be more "SFINAE-friendly" and avoid issues like the one you ran into. (Maybe somebody could check for other classes with similar issues and submit a proposal to the Library Working Group.)

  1. Change the move constructor to promise not to throw if the Container type makes the same promise, typically done with language like:

    explicit queue(Container&& rhs = Container()) nothrow(see below);
    

    Remarks: The expression inside noexcept is equivalent to is_­nothrow_­move_­constructible_­v<Container>.

  2. Change the copy constructor to be deleted if the Container type is not copyable, typically done with language like:

    explicit queue(const Container&);
    

    Remarks: This constructor shall be defined as deleted unless is_­copy_­constructible_­v<Container> is true.