Let's say I have some function a parameter type (or several parameter types) of type which I want to be deduced. Also I want different behavior based on the fact is it rvalue or lvalue. Straightforwardly writing it leads to an obvious (for experienced people) trap because of perfect forwarding:
#include <iostream>
#include <vector>
template <typename T>
void f (T &&v) // thought to be rvalue version
{
// some behavior based on the fact that v is rvalue
auto p = std::move (v);
(void) p;
}
template <typename T>
void f (const T &v) // never called
{
auto p = v;
(void) p;
}
int main ()
{
std::vector<int> x = {252, 135};
auto &z = x;
f (z);
std::cout << x.size () << '\n'; // woah, unexpected 0 or crash
}
Even though sneaky nature of such behavior is already an interesting point but my question is actually different - what is good, concise, understandable workaround for such situation?
If perfectly forwarded type is not deduced (e.g. it's already known template parameter of an outer class or something like this) there's well known workaround using typename identity<T>::type&&
instead of T&&
but since the same construction is a workaround for avoiding type deduction it doesn't help in this case. I could probably imagine some sfinae tricks to resolve it but code clarity would probably be destroyed and it will look completely different from the similar non-template functions.
If a function templates forward its arguments without changing its lvalue or rvalue characteristics, we call it perfect forwarding. Great. But what are lvalues and rvalues?
One can see forward as a pretty wrapper around static_cast<T&&> (t) when T can be deduced to either U& or U&&, depending on the kind of argument to the wrapper (lvalue or rvalue). Now we get wrapper as a single template that handles all kinds of forwarding cleanly. The forward template exists in C++11, in the <utility> header, as std::forward.
Rvalues cannot be bound to function parameters that are references, so the following completely reasonable calls will now fail: And no, making those reference parameters const won't cut it either, because func may legitimately want to accept non- const reference parameters.
The simplest situation is when ParamType is a reference type or a pointer type, but not a universal reference. In that case, type deduction works like this: If expr ’s type is a reference, ignore the reference part. Then pattern-match expr ’s type against ParamType to determine T. the deduced types for param and T in various calls are as follows:
SFINAE hidden in a template parameter list:
#include <type_traits>
template <typename T
, typename = typename std::enable_if<!std::is_lvalue_reference<T>{}>::type>
void f(T&& v);
template <typename T>
void f(const T& v);
DEMO
SFINAE hidden in a return type:
template <typename T>
auto f(T&& v)
-> typename std::enable_if<!std::is_lvalue_reference<T>{}>::type;
template <typename T>
void f(const T& v);
DEMO 2
In c++14 typename std::enable_if<!std::is_lvalue_reference<T>{}>::type
can be shortened to:
std::enable_if_t<!std::is_lvalue_reference<T>{}>
Anyway, even in c++11 you can shorten the syntax with an alias template if you find it more concise:
template <typename T>
using check_rvalue = typename std::enable_if<!std::is_lvalue_reference<T>{}>::type;
DEMO 3
With c++17 constexpr-if:
template <typename T>
void f(T&& v)
{
if constexpr (std::is_lvalue_reference_v<T>) {}
else {}
}
With c++20 concepts:
template <typename T>
concept rvalue = !std::is_lvalue_reference_v<T>;
void f(rvalue auto&& v);
void f(const auto& v);
DEMO 4
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