Forwarding of return values. Is std::forward is needed?
Asked Answered
P

4

38

I am writing library which wraps a lot of functions and methods from other library. To avoid coping of return values I am applying std::forward like so:

template<class T>
T&& wrapper(T&& t) { 
   f(t);  // t passed as lvalue  
   return std::forward<T>(t);
}

f returns void and takes T&& (or overloaded on valueness). Wrapper always returns wrappers's param and on returned value should preserve valuness of argument. Do I actually need to use std::forward in return? Does RVO makes it superfluous? Does the fact that it is a reference (R or L) makes it superfluous? Is it needed if return is not last function statement (inside some if)?

It is debatable if wrapper() should return void or T&&, because caller have access to evaluated value via arg (which is reference, R or L). But in my case I need to return value so that wrapper() can be used in expressions.

It might be irrelevant to the question, but it is known that functions f does not steal from t, so 1st use of std::forward in f(std::forward<T>(t)) is superfluous and it was removed by me.

I've wrote small test: https://gist.github.com/3910503

Test shows, that returning unforwarded T- does creates extra copy in gcc48 and clang32 with -O3 (RVO does not kicks in).

Also, I was not able to get bad behavior from UB in:

auto&& tmp = wrapper(42); 

It does not prove anything of cause because it is undefined behavior (if it is UB).

Portwin answered 18/10, 2012 at 6:4 Comment(11)
You worry about copying of return values but if there's a chance f steals away its parameter then what's the point of the return value at all?Extent
It is known that f won't steal it parameter. I agree that it is debatable why to return from wrapper, if return value is known (argument is reference). But I need to return, so that wrapper can be used in function-style expressions.Portwin
@LucDanton - after answering to you (and saying that f won't steal its parameters), I've realized that I probably do not need forward in f(std::forward<T>(t))Portwin
Well in an ideal world f would take T const& I suppose, in which case you wouldn't forward indeed. However I've found myself writing such function templates that take T&& even though they won't modify their parameters ever, for various reasons.Extent
I swear, if I see someone call a && a "universal reference" again...Ablution
@NicolBolas - you don't like the term, or I am using it incorrectly? I know of cause that T&& are not always universal references.Portwin
What is a "universal reference"?Stortz
@LeonidVolnitsky: The term "universal reference" is not proper C++ terminology; you will not find it in the C++ specification. At present, the only place I've heard it from is a presentation by Scott Meyers. And I really hope it doesn't catch on.Ablution
@Stortz - channel9.msdn.com/Shows/Going+Deep/…Portwin
@Nicol : Stephan T Lavavej from MS uses the term a lot as well, and given his plethora of Channel9 videos, the term is catching on, like it or not.Macdougall
@NicolBolas Still a better named idiom than RAII. Also, I heard "forwarding reference" is coming to replace it.Stilliform
E
13

In the case that you do know that t will not be in a moved-from state after the call to f, your two somewhat sensible options are:

  • return std::forward<T>(t) with type T&&, which avoids any construction but allows for writing e.g. auto&& ref = wrapper(42);, which leaves ref a dangling reference

  • return std::forward<T>(t) with type T, which at worst requests a move construction when the parameter is an rvalue -- this avoids the above problem for prvalues but potentially steals from xvalues

In all cases you need std::forward. Copy elision is not considered because t is always a reference.

Extent answered 18/10, 2012 at 7:19 Comment(3)
Would auto&& ref = wrapper(42) leave a dangling reference? I thought the lifetime of the temporary would be extended to the scope of ref (see first comment on #31273205 )Compile
@Compile The comment you mention first exhibits an example of a non-extended temporary, i.e. in auto&& tmp = id(5);. Here the temporary created from 5 only lives until the semicolon. This is the dangerous, non-obvious behaviour I’m warning about.Extent
@Compile The lifetime of a temporary is only extended if a temporary was bound by a reference (a.k.a when returning by value). If a function returns any kind of reference, even a r-value reference, it's not a temporary.Get
M
8

Depending on what this function gets passed, it results in undefined behavior! More precisely, if you pass a non-lvalue, i.e. an rvalue, to this function, the value referenced by the returned reference will be stale.

Also T&& isn't a "universal reference" although the effect is somewhat like a universal reference in that T can be deduced as T& or T const&. The problematic case is when it gets deduced as T: the arguments get passed in as temporary and die after the function returns but before anything can get hold of a reference to it.

The use of std::forward<T>(x) is limited to, well, forwarding objects when calling another function: what came in as a temporary looks like an lvalue within the function. Using std::forward<T>(x) lets x look like a temporary if it came in as one - and, thus, allow moving from x when creating the argument of the called function.

When you return an object from a function there are a few scenarios you might want to take care of but none of them involves std::forward():

  • If the type is actually a reference, either const or non-const, you don't want to do anything with the object and just return the reference.
  • If all return statements use the same variable or all are using a temporary, copy/move elision can be used and will be used on decent compilers. Since the copy/move elision is an optimization, it doesn't necessarily happen, however.
  • If always the same local variable or a temporary is returned it can be moved from if there is a move constructor, otherwise the object will be copied.
  • When different variables are returned or when the return involves an expression, references may still be returned but copy/move elision will not work, nor will it be directly possible to move from the result if isn't a temporary. In these cases you need to use std::move() to allow moving from the local object.

In most of these cases the produced type is T and you should return T rather than T&&. If T is an lvalue type the result may not be an lvalue type, though, and it may be necessary to remove the reference qualification from the return type. In the scenario you specifically asked about the type T works.

Metopic answered 18/10, 2012 at 6:34 Comment(6)
Prvalues may be problematic and can indeed result in stale references, but xvalues aren't as likely to end up with UB -- not that I find returning a moved-from value particularly useful.Extent
"if you pass an rvalue to this function, the value referenced by the returned reference will be stale". I'm probably being dense, but why is that then? If the caller passed in an rvalue to a temporary, doesn't that temporary live at least until the end of the caller's full-expression, hence long enough to be referenced by the return value of this function? Hence something like my_Foo_vector.emplace_back(wrapper(Foo());. I see that returning T would also allow that, I just don't see when returning T&& yields a stale reference.Biennial
The temporary you create, conceptually, isn't the object the function sees! The function sees, conceptually, a copy of this object. The life-time of the parameter ends earlier than the full expression in which temporary is created. In particular, 5.2.2 [expr.call], paragraph 4 contains the following sentence: "... The lifetime of a parameter ends when the function in which it is defined returns. .." In practice you will see the copy/move of this object being elided. Even if I got this logic wrong, the object is stale in the sense that it is being moved from, conceptually, when being passed on.Sligo
Actually, I think I have confused myself: the rvalue reference is still a reference, i.e., there shouldn't be any copy of the temporary and it lives long enough to return a reference to it. That said, there is still a problem but it is even deeper than I thought: in the call f(std::forward<T>(t)) the function f() get permission to move the object from t and referencing t inside wrapper() needs to assume that t is moved from. If f() doesn't move from the passed object, there is no need to use std::forward<T>() when calling f().Sligo
OK, slowly coming around: the call to f() shouldn't std::forward<T>(t) to the called function if the content of t can't be stolen: The purpose of forwarding to make it possible to have the content stolen. On the other hand, when returning t as a T&& it is necessary to restore rvalueness for t, i.e., to feed a return type of T&& using std::forward<T>(t) is what get's the return right. That said, I think the interface is still too fragile.Sligo
+1, thanks for your very detailed answer. I my particular case wrapper always returns wrappers's param and on return value should preserve valuness of argument.Portwin
P
3

No you dont need to use std::forward better dont return r-value reference at all because it can prevent of NRVO optimization. You can read more about move semantics in this article: Article

Pacifism answered 18/10, 2012 at 6:34 Comment(1)
(N)RVO cannot apply to function arguments, only to locals with automatic storage duration.Macdougall
C
0

Did some tests myself, and the following are the results:

template <class T>
T&& wrapper_without_forwarding(T&& t) {
    return t;
}

This compiles for these scenarios:

int&& ri = 123;
int& li = ri;
wrapper_without_forwarding(li);
wrapper_without_forwarding(ri);

But if I directly pass in a number like this:

wrapper_without_forwarding(123);

Compilation fails with the following messages:

error: cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’
   28 |     return t;
      |            ^

The error messages might be confusing, but personally, I think it is equivalent to this situation:

int&& test(){
    int&& rv = 123;
    return rv; // compilation error 
    return 123; // correct
}

Basically, the type of rv is indeed a rvalue reference, but it itself is not a rvalue. Also, the return type of int&& can only bind to rvalues so the compilation failed. I was initially also confused by the idea, but I found this video to be very helpful. A good rule of thumb to determine the lvalue-ness and rvalue-ness of a value is to see if a value has a name.

Back to the original wrapper_without_forwarding function. Since we're passing in a rvalue of 123, T&& deduced to int&& to bind the rvalue. At the same time, the return value of this function is also specified as int&& type, which goes into the exact same situation as the test function (return value have to be rvalue).

For these two lines wrapper_without_forwarding(li); and wrapper_without_forwarding(ri); Since li and ri are all lvalue. The forwarding reference T&& eventually deduces to T&, and you can always return the lvalue reference of a lvalue.

If we add std::forward, we essentially apply std::move to the cases where the forwarding reference is deduced to rvalue reference like int&& (like the previous example of passing in the number 123). We then consider what std::move is doing: converting anything (notice that a named variable of type 'rvalue reference' is still a lvalue) into rvalue.

int&& test(){
    int&& rv = 123;
    return std::move(rv); 
}

And this function will eventually work if we apply that std::move. However, std::move will convert anything into rvalue. But we don't want this to happen for cases like T&& become int&, so we apply std::forward.

In conclusion, if you do want to make the function wrapper work regardless of the type passed in, then std::forward should be applied.

Crocidolite answered 2/7 at 21:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.