Modern (Effective) C++ - Understanding std::move and std::forward

This article is inspired from Item 23 of Effective Modern C++ by Scott Meyers.

The primary purpose of std::move is to convert an Lvalue to Rvalue, which can potentially make it eligible to be moved. When the object cannot be moved, then copy semantics is used.

Take the following example -

#include <iostream>
#include <string>
void funcA(std::string& param)
{
std::string result(std::move(param));
std::cout << "Inside funcA - result : " << result << '\n';
}
void funcB(const std::string& param)
{
std::string result(std::move(param));
std::cout << "Inside funcB - result : " << result << '\n';
}
int main()
{
std::string str1 = "Effective Modern C++";
std::string str2 = "Scott Meyers";
std::cout << "Initial state of variables - " << str1 << " - " << str2 << '\n';
funcA(str1);
std::cout << "After passing str1 - " << str1 << '\n';
funcB(str2);
std::cout << "After passing str2 - " << str2 << '\n';
}
/* Output
Initial state of variables - Effective Modern C++ - Scott Meyers
Inside funcA - result : Effective Modern C++
After passing str1 -
Inside funcB - result : Scott Meyers
After passing str2 - Scott Meyers
*/

We see that funcA takes the parameter by reference. Since the parameter is not const, the underlying resource, i.e the character array is transferred to the result string. This leaves str1 as null ( nullptr ).
On the other hand, funcB takes the parameter by const reference. Even though std::move does change parameter into Rvalue, it's contents cannot be transferred because by the very definition, const variables cannot change. Hence instead of moving the parameter to result, the string is copied.

std::forward does the casting of it's parameters similar std::move, albeit differently. std::forward casts the argument to Rvalue, only if the argument is bound to an Rvalue. To understand this further, let's look at this example -

#include <iostream>
#include <string>
#include <memory>
#include <random>
#include <utility>
class A
{
std::unique_ptr<int> x;
public:
A()
{
x = std::make_unique<int>(rand() % 50);
}
A(A& other) : x(std::make_unique<int>(*other.x))
{
std::cout << "Copied from source " << *other.x << " to " << *x << '\n';
}
A(A&& other) : x(std::move(other.x))
{
std::cout << "Move Op called. Content is " << *x << '\n';
}
void display()
{
std::cout << "My content is " << *x << '\n';
}
};
void funcA(A&& param)
{
std::cout << "funcA rvalue param called - ";
param.display();
}
void funcA(A& param)
{
std::cout << "funcA lvalue param called - ";
param.display();
}
A returnA()
{
return A{};
}
template<typename T>
void displayWithoutForward(T&& param)
{
funcA(param);
}
template<typename T>
void displayWithForward(T&& param)
{
funcA(std::forward<T>(param));
}
int main()
{
srand(time_t(0));
std::cout << "Test Run 1\n";
A aObj;
displayWithoutForward(aObj);
displayWithoutForward(returnA());
std::cout << "\nTest Run 2\n";
A bObj;
displayWithForward(bObj);
displayWithForward(returnA());
}
/* Output
Test Run 1
funcA lvalue param called - My content is 33
funcA lvalue param called - My content is 36
Test Run 2
funcA lvalue param called - My content is 27
funcA rvalue param called - My content is 15
*/

Lets first examine the "Test Run 1" i.e the calls to displayWithoutForward.
The first call is invoked, with aObj, a Lvalue. So when, displayWithoutForward calls funcA, it calls the Lvalue version.
The second call is invoked with return value of function, returnA, a Rvalue. But when, displayWithoutForward calls funcA, the parameter loses it "Rvalueness" and Lvalue version of funcA is called.


Now examining the "Test Run 2" i.e the calls to displayWithForward.
The first done is invokedwith bObj, a Lvalue. So when, displayWithForward calls funcA, it calls the Lvalue version.The call to std::forward<T> does not have any effect.
The second call is invoked with return value of function, returnA, a Rvalue. When displayWithForward calls funcA with std::forward<T>, the parameter retains it 's "Rvalueness" and the Rvalue version of funcA is called. This feature is also called perfect forwarding.

Hence std::forward only casts the argument to Rvalue conditionally, whereas std::move casts the argument to Rvalue unconditionally.

To use std::forward<T> , include<utility>.



Modern (Effective) C++ - Understanding std::move and std::forward Modern (Effective) C++ - Understanding std::move and std::forward Reviewed by zeroingTheDot on February 11, 2018 Rating: 5

No comments:

Powered by Blogger.