0

I want to write a template which can turn lvalue/rvalue to rvalue using universal reference like std::forward For rvalue, just forward it. For lvalue, copy it.

it is used as below

template<typename T>
void func(T&& arg) {
    f_take_rv(make_rv<T>(arg)); // f_take_rv takes right value only
}

Here is my implement

template<typename T> // for lvalue
constexpr T make_rv(typename std::remove_reference<T>::type& arg) {
    return arg;
}
template<typename T> // for rvalue
constexpr T&& make_rv(typename std::remove_reference<T>::type&& arg) {
    return static_cast<T&&>(arg);
}

However, it always goes to the lvalue one

I also tried std::enable_if to control its type deduction.

template<typename T>
constexpr typename std::enable_if<
    std::is_reference<T>::value,
    typename std::remove_reference<T>::type
>::type make_rv(typename std::remove_reference<T>::type& arg) { return arg; }

template<typename T>
constexpr typename std::enable_if<
    !std::is_reference<T>::value,
    T
>::type&& make_rv(typename std::remove_reference<T>::type&& arg) {
    return static_cast<T&&>(arg);
}

But it failed for literal string, like "str": no matching function for call to 'make_rv<const char(&)[4]>(const char [4])'

Could you tell me how to implement it

gcc implement of std::forward for reference:

template<typename _Tp>  // forwarding an lvalue
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept {
    return static_cast<_Tp&&>(__t);
}
template<typename _Tp>  // forwarding an rvalue
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept {
    return static_cast<_Tp&&>(__t);
}
4
  • constexpr T make_rv(), However, it always goes to the lvalue one - it is rvalue, a returned temporary value can't be lvalue. Commented Jul 5, 2023 at 2:05
  • yes, my purpose is to return a right value, for rvalue, forward it, for lvalue, copy and return it Commented Jul 5, 2023 at 2:10
  • 2
    arg is an lvalue, even when T&& is deduced to be rvalue reference. Rule of thumb: if it has a name, it's an lvalue. Look at the documentation for std::forward; note that the second overload is called only when the code does something like std::forward<T>(f()), forwarding the return value of a function. Commented Jul 5, 2023 at 2:11
  • 2
    The approach with enable_if should work, except that both overloads should take type& arg. You dispatch not on whether arg is an lvalue or rvalue, but on whether template parameter T is a reference type. Commented Jul 5, 2023 at 2:14

3 Answers 3

1

If I understand you correctly, nothing fancy is required:

template <typename T>
typename std::remove_const<T>::type make_rv(T& t)
{
    return t;
}

template <typename T>
T&& make_rv(T&& t)
{
    return std::forward<T>(t);
}
Sign up to request clarification or add additional context in comments.

1 Comment

Hi @Joseph Thomson, In my use case, make_rv is used like std::forward. That is to say make_rv always take lvalue, and whether it originally was r/l value is presented in T with universal reference
1

I would go probably the easiest route possible:


template<typename T>
constexpr auto copy_l_forward_r(T&& arg)
    -> typename std::conditional<std::is_lvalue_reference<T>::value && !std::is_array<T>::value,
        typename std::remove_cv<typename std::remove_reference<T>::type>::type, T&&>::type

{   
    return std::forward<T>(arg);
}

https://godbolt.org/z/WbbWPa5PE

The main question is what to do about references to C-style arrays. They can be returned "as-is" (thanks @JosephThomson for that comment), but they can also be decayed to a pointer.

To expand it a bit more: the whole confusion (as this is a recurring topic in one form or another) stems from the way type deduction works in C++.

Consider a function taking a forwarding/universal reference: template<typename T> void f(T&& f) {/*whatever*/} Imagine it's called with an integer literal: f(3); What are types T and arg are going to be? Hint: they're not going to be the same. T is an int, decltype(arg) is is int&&.

Ok, so what about int x = 4; f(x);? T is int&, decltype(arg) int&. Ok, so far so good.

Now the interesting part comes. Consider the following code snippet:

static int i = 3;
int& lvint() { return i; }
int&& rvint() {return std::move(i);}

What are the types inside f going to be? f(lvint()); Now, both T and decltype(arg) are int&. Still, nice, easy and predictable.

Now take: f(rvint()); and this is where the plot thickens. decltype(arg) is int&&, but T is int. Not a reference.

I cannot point my finger at the exact chapter in the standard that is responsible for this, the closest one I can think of is this:

A forwarding reference is an rvalue reference to a cv-unqualified template parameter that does not represent a template parameter of a class template (during class template argument deduction ([over.match.class.deduct])). If P is a forwarding reference and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction. https://eel.is/c++draft/temp.deduct.call

My personal rule of thumb (but just a personal opinion, so feel free to disagree) is this: it is more predictable to operate on the decltype(arg) rather than T when fiddling with perfect forwarding. Or, when operating on T: keep in mind that T can be a non-reference type unless it's a lvalue one.

And some examples to illustrate all the above: https://godbolt.org/z/dzzqdrM8T

6 Comments

Your implementation fails for const lvalues and array references.
@JosephThomson how does it fail for const T&? static_assert(std::is_same_v<decltype(copy_l_forward_r(clvint())), int>, ""); seems fine, can you show the exact case?
Sorry, the function itself won't fail, but it will return const T when you pass it const T&, which will fail to bind to T&&. Changing std::remove_reference_t<T> to std::remove_cvref_t<T> should fix it (though neither is C++11 compatible).
And I think the array reference incompatibility can be fixed by changing std::is_lvalue_reference_v<T> to std::is_lvalue_reference_v<T> && !std::is_array_v<std::remove_reference_t<T>>.
@JosephThomson OK I see now. But interestingly enough, it returns int (non-const) for const int& argument, so there's probably sth more at play that we're missing.
|
1

I found a version works for me

template<typename T> // for lvalue, copy it
constexpr typename std::enable_if<
    std::is_reference<T>::value,
    typename std::decay<T>::type
>::type make_rv(const T& arg) { return arg; }

template<typename T> // for rvalue, forward it
constexpr typename std::enable_if<
    !std::is_reference<T>::value,
    T
>::type&& make_rv(T& arg) {
    return static_cast<T&&>(arg);
}

4 Comments

does it? godbolt.org/z/dxobjjhcs Maybe my test covers case which you don't use, but still, are you sure?
Is there any reason you're returning T for rvalues and not T &&?
@HolyBlackCat I am returning T for lvalue, T&& for rvalue
Hi @alagner, make_rv is used like std::forward. That is to say make_rv always take lvalue, and whether it originally was r/l value is presented in T with universal reference

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.