C++ Right Value Semantic Foundation--Perfect Forwarding

Posted by mattastic on Fri, 04 Feb 2022 18:56:51 +0100


Personal Site Version Link

What is perfect forwarding?

Familiar with modern C++ syntax, it should be clear that C++ divides variables into left and right values. In order to transfer resources instead of copying, right values and corresponding mobile constructors arise, but we find that many times we can't precisely transfer left and right values to corresponding versions of functions for processing, such as the following simple code. You will find that even if we set the parameter type of a function to a right-value reference, when we use it to call the corresponding constructor, it gives us a copy construct! So this forwarding isn't perfect enough!

#include<iostream>
using namespace std;
class test{
public:
    test() = default;
    test(test&& p){
        cout<<"move construct call"<<endl;
    }
    test(const test& p){
        cout<<"copy construct call"<<endl;
    }
};
void test_fun(test&& p){
    test q(p);
    return;
}

int main() {
    test_fun(test());
    return 0;
}

Why is this happening?

Because the parameter passed in is either a left or a right value, it is considered a left value because it has both a name and an addressable address within the function.

How to achieve perfect forwarding?

Perfect forwarding is easy. We only need the template function forward<T> in modern C++. The actual principle is to use the syntax of folding references provided in the C++11 template. The ultimate effect is to cast the type of parameter to its own type, left to left, right to right. This allows you to call which version of the function you want to call and which version of the function you want to call. It is no longer just a right value!

Previous code can do this for perfect forwarding:

#include<iostream>
using namespace std;
class test{
public:
    test() = default;
    test(test&& p){
        cout<<"move construct call"<<endl;
    }
    test(const test& p){
        cout<<"copy construct call"<<endl;
    }
};
void test_fun(test&& p){
    test q(forward<test>(p)); //Modified Places
    return;
}

int main() {
    test_fun(test());
    return 0;
}

Perfect forwarding is indispensable to any template library

In fact, as long as it is a C++ template library, no one does not need perfect forwarding. At the same time, the problem of perfect forwarding arises from templates. However, the implementation of forward function is not difficult. In fact, it uses the omnipotent folding semantics provided by C++11 for templates:

  • When the argument is a left value or a left value reference (A&), T&& in the function template will be converted to A& (A& && = A&);
  • When an argument is a right value or a right value reference (A&&&), T&& in the function template will be converted to A&& (A&& && = A&&&).

Here's a simple factory created with perfect forwarding design:

#include<iostream>
#include <memory>

using namespace std;
class test{
public:
    test() = default;
    test(int&& arg):m_iData(arg){
        cout<<"move construct call"<<endl;
    }
    test(const int& arg):m_iData(arg){
        cout<<"copy construct call"<<endl;
    }
private:
    int m_iData;
};

template<typename T,typename Arg>
//The reason not to use T&&directly is that using only one template parameter will result in a factory parameter not having the effect of a universal reference
shared_ptr<T> factory(Arg&& arg){
    return shared_ptr<T>(new T(forward<Arg>(arg)));//Call the correct constructor with perfect forwarding
}

int main() {
    int val = 5;
    auto p1 = factory<test>(val);
    auto p2 = factory<test>(5);
    return 0;
}

std::forward implementation principle

The source code for gcc is implemented as follows:

template<typename _Tp>
constexpr _Tp &&
forward(typename std::remove_reference<_Tp>::type &__t) noexcept { return static_cast<_Tp &&>(__t); }

template<typename _Tp>
constexpr _Tp &&
forward(typename std::remove_reference<_Tp>::type &&__t) noexcept {
    static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
                                                         " substituting _Tp is an lvalue reference type");
    return static_cast<_Tp &&>(__t);
}

We found two template specializations implemented in the source code, _ Tp&and_ Tp & & but ultimately all through static_cast +Collapses the properties of references to achieve a forced transformation. It's just a simple improvement!

Harvest

When designing template libraries, if you need to forward based on a semantically clear right value of left value, be sure to use forward, otherwise the parameter will only be treated as left value!

Topics: C++ Back-end