Search code examples
c++c++11perfect-forwardingstdmove

Usage of std::forward vs std::move


I always read that std::forward is only for use with template parameters. However, I was asking myself why. See the following example:

void ImageView::setImage(const Image& image){
    _image = image;
}

void ImageView::setImage(Image&& image){
    _image = std::move(image);
}

Those are two functions which basically do the same; one takes an l-value reference, the other an r-value reference. Now, I thought since std::forward is supposed to return an l-value reference if the argument is an l-value reference and an r-value reference if the argument is one, this code could be simplified to something like this:

void ImageView::setImage(Image&& image){
    _image = std::forward(image);
}

Which is kind of similar to the example cplusplus.com mentions for std::forward (just without any template parameters). I'd just like to know, if this is correct or not, and if not why.

I was also asking myself what exactly would be the difference to

void ImageView::setImage(Image& image){
    _image = std::forward(image);
}

Solution

  • You cannot use std::forward without explicitly specifying its template argument. It is intentionally used in a non-deduced context.

    To understand this, you need to really understand how forwarding references (T&& for a deduced T) work internally, and not wave them away as "it's magic." So let's look at that.

    template <class T>
    void foo(T &&t)
    {
      bar(std::forward<T>(t));
    }
    

    Let's say we call foo like this:

    foo(42);
    
    • 42 is an rvalue of type int.
    • T is deduced to int.
    • The call to bar therefore uses int as the template argument for std::forward.
    • The return type of std::forward<U> is U && (in this case, that's int &&) so t is forwarded as an rvalue.

    Now, let's call foo like this:

    int i = 42;
    foo(i);
    
    • i is an lvalue of type int.
    • Because of the special rule for perfect forwarding, when an lvalue of type V is used to deduce T in a parameter of type T &&, V & is used for deduction. Therefore, in our case, T is deduced to be int &.

    Therefore, we specify int & as the template argument to std::forward. Its return type will therefore be "int & &&", which collapses to int &. That's an lvalue, so i is forwarded as an lvalue.

    Summary

    Why this works with templates is when you do std::forward<T>, T is sometimes a reference (when the original is an lvalue) and sometimes not (when the original is an rvalue). std::forward will therefore cast to an lvalue or rvalue reference as appropriate.

    You cannot make this work in the non-template version precisely because you'll have only one type available. Not to mention the fact that setImage(Image&& image) would not accept lvalues at all—an lvalue cannot bind to rvalue references.