Martin Castin
Martin Castin

Reputation: 163

Is there a way to transparently use move or copy constructor in a template?

I am trying to implement a generic thread safe queue in a way that can work both with move-only objects or copy-only objects. Here is what I tried (I removed all the irrelevant code (locks) for the sake of simplicity):

struct MoveOnly
{
  MoveOnly() = default;
  MoveOnly(const MoveOnly& a) = delete;
  MoveOnly& operator=(const MoveOnly& a) = delete;
  MoveOnly(MoveOnly&& a) = default;
  MoveOnly& operator=(MoveOnly&& a) = default;

  std::vector<int> v;

};

struct CopyOnly
{
  CopyOnly() = default;
  CopyOnly(const CopyOnly &a) = default;
  CopyOnly &operator=(const CopyOnly &a) = default;
  CopyOnly(CopyOnly &&a) = delete;
  CopyOnly &operator=(CopyOnly &&a) = delete;

  std::vector<int> v;
};

template <typename T>
class Queue
{
  std::queue<T> q;

public:
  T pop()
  {
    T t = q.front();
    return t;
  }

  void push(T&& t)
  {
    q.push(std::forward<T>(t));
  }
};

int main()
{
  Queue<MoveOnly> qm;
  qm.push(MoveOnly());
  MoveOnly mo = qm.pop();

  Queue<CopyOnly> qc;
  CopyOnly c;
  qc.push(c);
  CopyOnly&& co = qc.pop();
}

There are several compile errors due to pop: T t = q.front() cannot work with move semantics as the function returns a lvalue reference. T t = std::move(q.front()) would not work with an explicitly deleted move constructor as operator overloading will resolve to the deleted constructor. The same kind of problem used to appear in the push function but it is resolved with perfect forwarding.

Another problem is apparently that return t binds to the move constructor for CopyOnly, and I have difficulties to understand why it does so.

Is there a way to make pop work with both MoveOnlyand CopyOnly objects?


Side question: does it make sense to define an object like CopyOnlywith explicitly deleted move constructors? In which case would it be useful to do that? Because it works if the constructors are implicitly deleted.

Upvotes: 3

Views: 200

Answers (2)

Ted Lyngmo
Ted Lyngmo

Reputation: 117573

You could use constexpr if and check if T is std::move_constructible_v.

I'd also create an emplace proxy:

#include <type_traits>

template<typename T>
class Queue {
    std::queue<T> q;

public:
    decltype(auto) pop() {
        if constexpr(std::is_move_constructible_v<T>) {
            T t = std::move(q.front());
            q.pop();
            return t;
        } else {
            T t = q.front();
            q.pop();
            return t;
        }
    }

    template<class... Args>
    decltype(auto) emplace(Args&&... args) {
        return q.emplace(std::forward<Args>(args)...);
    }
};

Here's a C++11 version (I didn't notice the C++11 tag before). Deleting the move constructor and move assignment operator in CopyOnly really made a mess out of it. You should probably never do that in real code.

To get CopyOnly co = qc.pop(); to work, pop() needs to return a const T or else the move constructor will be part of the overload resolution, which it still will be even though it's deleted, but compilation will fail just because it's deleted.

If CopyOnly&& co = qc.pop(); is ok for you, you can replace the const U with U in the enable_if.

template<typename T>
class Queue {
    std::queue<T> q{};

public:
    template<typename U = T>
    typename std::enable_if<std::is_move_constructible<U>::value, U>::type
    pop() {
        U t = std::move(q.front());
        q.pop();
        return t;
    }

    template<typename U = T>
    typename std::enable_if<!std::is_move_constructible<U>::value, const U>::type
    pop() {
        U t = q.front();
        q.pop();
        return t;
    }

    template<class... Args>
    void emplace(Args&&... args) {
        q.emplace(std::forward<Args>(args)...);
    }
};

Here's another C++11 version built on rafix07's idea with an extra popper type that does the pop after return to take care of a possible bug in gcc 7.3.

template<typename T>
class Queue {
    std::queue<T> q{};

    struct popper {
        std::queue<T>& q_ref;
        ~popper() {
            q_ref.pop();
        }
    };
public:
    using Type = typename
        std::conditional<std::is_move_constructible<T>::value, T, T&>::type;

    T pop() {
        popper pop_after_return{q};
        return std::forward<Type>(q.front());
    }

    template<class... Args>
    void emplace(Args&&... args) {
        q.emplace(std::forward<Args>(args)...);
    }
};

Upvotes: 5

rafix07
rafix07

Reputation: 20934

You can add to your queue:

using Type = std::conditional_t< std::is_move_assignable_v<T> &&
   std::is_move_constructible_v<T>, T, T&>;

then pop looks like:

T pop()
{
    std::decay_t<Type> t = std::forward<Type>(q.front());
    q.pop();
    return t;
}

if T is moveable, by std::forward<Type>(q.front()) move constructor is called (front is casted to be rvalue reference to trigger move construction). For T as copyable, copy construction will be performed.

Demo

Upvotes: 2

Related Questions