Saturday, February 8, 2020

More on std::move and std::forward

In the an earlier post regarding moves in C++, I explained what rvalue references are and did a speed comparison of a move with a copy. Here in this post, I would be highlighting the need of std::move and std::forward in a C++ program. Before that, let's just quickly see what an std::move and std::forward really do. I found this interesting description from one of the presentations by Scott Meyers.
What std::move does?
- it doesn't really move
- it does a rvalue cast on the object
Note, that decision to move or not is taken by the overload resolution process which looks at whether it can call a move assignment/constructor or should it fallback to the "copy" counterparts. For example:
class string
{
    string& operator = (const string&); // copy assignment
    string& operator = (string&&); // move assignment
 ...
}
void foo(const std::string s)
{
   std::string other_str;
   other_str = std::move(s); // still, calls copy assignment because 's' is const
}

What std::forward does?
- it does not really forward
- it does a conditional cast to the rvalue, unlike std::move where the cast is unconditional.

Note: Both std::move and std::forward accept a template type argument, but with std::forward template type deduction is forbidden, while with std::move it is allowed. So, you need to call std::forward always with an explicitly specified template type. This is needed because otherwise there is no way to know with-in std::forward(T&& arg)'s definition if the argument was bound to a lvalue or rvalue. This is because every argument has a name, which causes compiler to always deduce a lvalue-reference type for it. So:
- Calling std::forward<T&>(x) implies 'x' is bound to a lvalue (going by reference collapsing rules)
- Calling std::forward<T>(x) implies 'x' is bound to a rvalue.
A profuse explanation of this can be found in this stackoverflow post
Corollary: std::move is exactly equivalent to a 'std::forward<T>'.

So, let's see the usage of std::move and std::forward with-in the programs, beginning with std::move.

std::move scenarios
1. A class constructor accepting a rvalue reference that needs to be moved into one of its members:
class Test
{
    std::vector mVec;
    public:
        // std::move needed because 'in' is a lvalue (as it has a name, though it is of rvalue-ref type)
        Test(std::vector&& in) : mVec(std::move(in)) { }
        
        Test& add (const Test& t) { ... }

        Test& add (Test&& t) { ... }
...
}
2. A function returning by value, returns a name bound to a rvalue reference
Test foo(Test&& x) {
  x.add(std::vector({1, 22, 13, 56}));
  return std::move(x); // std::move required because x is a l-value, otherwise it will copy constructor
}
3. To invoke the move constructor/assignment when moving named objects:
    Test t (std::vector({1, 22, 13, 56}));
    Test t2 = std::move(t); // use std::move to invoke the move constructor or move assignment
4. To create a rvalue reference seating a named object:
    Test&& rt2 = std::move(t2); // use std::move to create a rvalue reference
5. To invoke a rvalue overload of a function:
   t1.add(std::move(rt2)); // use std::move to invoke rvalue ref overload of a function
Full example code can be found at std_move_examples.cpp

std::forward scenarios
For std::forward, there can be similar scenarios, but it's mainly used in the context of arguments passing to preserve move semantics during calls. Basically, its used in the context of "universal references" i.e. on name of templatized types like T&&, auto type like auto&& where type-deduction distinguishes between whether the reference is bound to a lvalue or rvalue.

1. To pass on the argument preserving its reference-ness type:
void bar(std::string& x) { ... }
void bar(std::string&& x) { ... }
template<typename T>
void foo(T&& x)
{
  bar(std::forward<T>(x));
}
int main()
{
  auto f = [](){ std::string s = "I am R value"; return s; }; // a rvalue reference creator
  std::string&& rrs = f(); // a rvalue reference
  foo(rrs); // will call the lvalue version of bar, as rrs is a name
  foo(f()); // will call the rvalue version
}
In the first call to foo, T is inferred as std::string&, while in the second call its inferred as std::string. The std::forward typecast to x ensures that its holds its intended reference-ness. A rough definition of std::forward would clarify this further:
template<typename T>
T&& forward(T&& param) {
   return static_cast<T&&>(param);
}
2. When moving/passing arguments inside a universal constructor
class Test
{
std::string ob;
public:
    // INCORRECT: moving a univeral reference is wrong as it may be bound to a l-value
    template <typename T>
    Test(T&& t) : ob(std::move(t)) { }
};
Use of std::move is wrong as it will unintentionally move the contents of a l-value, if it is passed to the constructor. Hence, std::move should be replaced by std::forward. Full code for these 2 cases can be found at std_forward_example.cpp

No comments:

Post a Comment