Reputation: 163
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 MoveOnly
and CopyOnly
objects?
Side question: does it make sense to define an object like CopyOnly
with 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
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
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.
Upvotes: 2