I made a small 'blocking queue' class. It irritates me that I have created redundant code for values passed into the enqueue
member function.
Here are the two functions that do the same exact thing (except the rvalue uses std::move to move the rvalue into the actual queue collection), except handles lvalue and rvalue respectively:
void enqueue(const T& item)
{
std::unique_lock<std::mutex> lock(m);
this->push(item);
this->data_available = true;
cv.notify_one();
}
void enqueue(T&& item)
{
std::unique_lock<std::mutex> lock(m);
this->push(std::move(item));
this->data_available = true;
cv.notify_one();
}
My question is, is there a way to combine these two functions, without losing support for rvalue references.
In the example, the main function passes an rvalue to f . The body of f treats its named parameter as an lvalue. The call from f to g binds the parameter to an lvalue reference (the first overloaded version of g ). You can cast an lvalue to an rvalue reference.
Pass by reference allows us to pass arguments to a function without making copies of those arguments each time the function is called. Pass by reference allows us to change the value of an argument.
What is Perfect Forwarding. Perfect forwarding allows a template function that accepts a set of arguments to forward these arguments to another function whilst retaining the lvalue or rvalue nature of the original function arguments.
Functions can be declared to return a reference type. There are two reasons to make such a declaration: The information being returned is a large enough object that returning a reference is more efficient than returning a copy. The type of the function must be an l-value.
This is a classic example of the need to perfectly forward. Do this by templating the function (member template if this is a member function):
template <class U>
void enqueue(U&& item)
{
std::unique_lock<std::mutex> lock(m);
this->push(std::forward<U>(item));
this->data_available = true;
cv.notify_one();
}
Explanation: If you pass an lvalue T
to enqueue
, U
will deduce to T&
, and the forward
will pass it along as an lvalue, and you'll get the copy behavior you want. If you pass an rvalue T
to enqueue
, U
will deduce to T
, and the forward
will pass it along as an rvalue, and you'll get the move behavior you want.
This is more efficient than the "pass by value" approach in that you never make an unnecessary copy or move. The downside with respect to the "pass by value" approach is that the function accepts anything, even if it is wrong. You may or may not get cascaded errors down under the push
. If this is a concern, you can enable_if enqueue
to restrict what arguments it will instantiate with.
Update based on comment
Based on the comments below, this is what I understand things look like:
#include <queue>
#include <mutex>
#include <condition_variable>
template <class T>
class Mine
: public std::queue<T>
{
std::mutex m;
std::condition_variable cv;
bool data_available = false;
public:
template <class U>
void
enqueue(U&& item)
{
std::unique_lock<std::mutex> lock(m);
this->push(std::forward<U>(item));
this->data_available = true;
cv.notify_one();
}
};
int
main()
{
Mine<int> q;
q.enqueue(1);
}
This is all good. But what if you try to enqueue a double instead:
q.enqueue(1.0);
This still works because the double is implicitly convertible to the int. But what if you didn't want it to work? Then you could restrict your enqueue
like this:
template <class U>
typename std::enable_if
<
std::is_same<typename std::decay<U>::type, T>::value
>::type
enqueue(U&& item)
{
std::unique_lock<std::mutex> lock(m);
this->push(std::forward<U>(item));
this->data_available = true;
cv.notify_one();
}
Now:
q.enqueue(1.0);
results in:
test.cpp:31:11: error: no matching member function for call to 'enqueue'
q.enqueue(1.0);
~~^~~~~~~
test.cpp:16:13: note: candidate template ignored: disabled by 'enable_if' [with U = double]
std::is_same<typename std::decay<U>::type, T>::value
^
1 error generated.
But q.enqueue(1);
will still work fine. I.e. restricting your member template is a design decision you need to make. What U
do you want enqueue
to accept? There is no right or wrong answer. This is an engineering judgment. And several other tests are available that may be more appropriate (e.g. std::is_convertible, std::is_constructible, etc.). Maybe the right answer for your application is no constraint at all, as first prototyped above.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With