Bottom Source Profiling Move, Right Value, Perfect Forwarding
1 Basic concepts
-
Moving semantics: Do not copy the object, just make the new object name point to the object referred to by the original object name, and leave the original object pointed to empty.
-
Perfect Forwarding: You can write a function template that accepts any parameter and forward it to another function, where the target function accepts exactly the same parameters as its forward function.
-
Right Reference: The underlying language mechanism that combines mobile semantics with perfect forwarding.
2 Source Profiling
2.1 std::move
move source code:
//c++11 template<typename T> typename std::remove_reference<T>::type&& move(T&& param){ using ReturnType = typename std::remove_reference<T>::type&&; return static_cast<ReturnType>(param); }
//c++14 template<typename T> decltype(auto) move(T&& param){ using ReturnType = std::remove_reference_t<T>&&; return static_cast<ReturnType>(param); }
⚠️ Note: std::remove_ Reference <T>:: Type means T generated by T&or T&
As the source code shows, the argument is cast to the right value. A right value is one that can be moved, and applying std::move to an object tells the compiler that the object has the conditions to move.
Move operation ⚠️:
If you want the ability to move an object, do not declare it constant. Move operations performed on constants will be transformed into copy operations.
std::move does not actually move anything, nor does it guarantee that the object after its forced type conversion will be movable.
For example, text is not moved, but copied into value:
class Annotation{ public: explicit Annotation(const std::string text):value(std::move(text)){} private: std::string value; };
//Called string class string{ public: string(const string& rhs); string(string&& rhs); };
2.2 std::forward
The source code is as follows:
- Version 1 is used to convert left values to left or right values, depending on T;
- Version 2 is used to convert right values to right values and prohibits right values to left values.
//Version 1 template<typename T> T&& forward(typename std::remove_reference<T>::type& param){ return static_cast<T&&>(param); } //Version 2 template <class T> T&& forward(typename std::remove_reference<T>::type&& param) { static_assert(!std::is_lvalue_reference<T>::value, "can not forward an rvalue as an lvalue"); return static_cast<T&&>(param); }
std::forward will only cast cast its arguments to the right value type under certain conditions [when initialization is initially completed using the right value], while std::move will unconditionally convert its arguments to the right value type.
Forward ⚠️:
Whether the original type of the object is left-valued or right-valued, it will remain the same.
The argument type passed to std::forward should be a non-reference type, since it is customary for the argument it encodes to pass to be a right value;
In the following example, if std::forward is not used, then the left-value version of process will be invoked.
void process(const Widget& lval){ std::cout<<"lval"<<std::endl; } void process(Widget&& rval){ std::cout<<"rval"<<std::endl; } template<typename T> void logAndProcess(T&& param){ process(std::forward<T>(param)); // process(param); } int main(){ Widget w; logAndProcess(w); logAndProcess(std::move(w)); }
2.2.1 std::forward's Operating Mechanism - Reference Folding
Rules for reference collapse (the end result is a single reference [available only in compiler context]):
If any of the original references are left-value references, the result is left-value references; Otherwise, the result is a right-value reference.
Reference Folding Context:
- Template instantiation;
- auto type generation;
- Create and use typedef and alias declarations;
- decltype
In C++ syntax, the user uses references by himself, which is illegal. However, it is not illegal for a compiler to generate references (template instantiation) in a particular context.
The following is illegal:
int x = 10; auto& &rh = x; //error: 'rh' declared as a reference to a reference
2.2.1.1 Template Instantiation
std::forward source code:
template<typename T> T&& forward(typename std::remove_reference<T>::type& param){ return static_cast<T&&>(param); }
Instance code:
template<typename T> void someFunc(T param){} template<typename T> void func (T&& fParam){ someFunc(std::forward<T>(fParam)); } class TestWidget{}; TestWidget widgetFactory() //Functions that return the right value { TestWidget t; return t; } int main(){ TestWidget t; func(t);//Left Value Call func(widgetFactory());//Right Value Call }
In the example above, both examples are called version 1 in std::forward, which is the code above. Because when passed to the function func, the parameter fparam inside the function body is left-valued.
Let's first look at passing a left-value call:
- 1. In function func, T is deduced as TestWidget &, fParam is of type TestWidget & &&, and reference collapsed is of type TestWidget &;
- 2, std::forward<T> (fParam), as the source code of std::forward, the template has been made into the following code (use TestWidget&instead of T). The following code can also be seen by referencing the collapsed result, because the type of param is TestWidget&and the final result after std::forward processing is TestWidget&
- 3. The last type that someFunc handles is TestWidget &.
Left-valued calls present results:
TestWidget& && forward(TestWidget& param){ return static_cast<TestWidget& &&>(param); }
Left value invokes the collapsed result of the reference:
TestWidget& forward(TestWidget& param){ return static_cast<TestWidget&>(param); }
Now let's look again at passing a right-value call:
- 1. In function func, T is deduced as TestWidget,fParam is of type TestWidget &&, and reference collapsed is of type TestWidget &&;
- 2, std::forward<T> (fParam), the source code of std::forward shows that the template has been made into the following code (using TestWidget instead of T). The following code can also be seen by referencing the collapsed result, because the type of param is TestWidget &, the final result after std::forward processing is TestWidget &&;
- 3. The last type that someFunc handles is TestWidget &&.
Right value calls present results:
TestWidget& && forward(TestWidget& param){ return static_cast<TestWidget &&>(param); }
2.2.1.2 auto type generation
The type derivation of auto variables is essentially the same as that of template types. Let's look at the following case.
In the following case, in auto&t1, the initial T1 is the left value, so T1 is derived from auto to TestWidget &, after replacing auto, TestWidget &&t1 is obtained, after the reference is collapsed, the type of T1 is TestWidget &; Similarly, t2 is pushed into a TestWidget by auto and brought in to get t2 of type TestWidget &&.
TestWidget t; func(t);//Left Value Call func(widgetFactory());//Right Value Call auto&& t1 = t; auto && t2 = widgetFactory();
2.2.1.3 typedef and alias declaration
In the following case, bringing int&into the location of T in typedef T&RvalueRefToT will result in typedef int&RvalueRefToT;.
template<typename T> class TestWidget{ public: typedef T&& RvalueRefToT; }; int main(){ TestWidget<int&> t; }
2.2.1.4 decltype
decltype is generally used to derive the return type of an expression, such as the following example. If a reference to a reference is encountered, reference collapse is also used to eliminate it.
template<typename Container, typename Index> decltype(auto) authAndAccess(Container&& c, Index i){ return c[i]; }
2.3 std::move and std::forward difference
std::move:unconditionally cast the right value type; std:;forward performs a cast to the right value type only for references bound to the right value.
Advantages of std::move:
- Convenient;
- Reduce the possibility of error;
- More Clear
std::forward is only used to pass an object to another function.
Example [Counting the number of mobile construction operations]:
class string{ public: string(const char* cp){std::copy(cp, cp+std::strlen(cp), std::back_inserter(data));} string(const string& rhs){std::cout<<"string Copy"<<std::endl;} string(string& rhs){std::cout<<"string Copy"<<std::endl;} string(string&& rhs){std::cout<<"string move"<<std::endl;} private: std::vector<char> data; }; class Widget{ public: Widget():s("123qwe"){} //Correct practices Widget(Widget&& rhs):s(std::move(rhs.s)){ ++moveCtorCalls; std::cout<<moveCtorCalls; } // Widget(Widget&& rhs):s(std::forward<string>(rhs.s)){ // ++moveCtorCalls; // std::cout<<moveCtorCalls<<std::endl; } private: static std::size_t moveCtorCalls; string s; }; std::size_t Widget::moveCtorCalls = 0; int main(){ Widget w; Widget w2(std::move(w)); Widget w3(std::move(w)); }
3 Use scenarios
3.1 Right value reference (std::move) and universal reference (std::forward)
Implement std::move for right value references and std::forward for universal references.
- Right value reference:
class Widget{ public: Widget(Widget&& rhs):name(move(rhs.name)),p(std::move(rhs.p)){} private: std::string name; std::shared_ptr<testWidget> p; };
- Universal Reference:
class Widget{ public: template<class T> void setName(T&& newName){ name = std::forward<T>(newName); } private: std::string name; std::shared_ptr<testWidget> p; };
If std::forward is used where the reference to the right value should be used, then the case where the parameter should be of the right value type becomes that of the left value if the parameter is of the left value.
If you use a universal reference, std::move, as in the following example, will cause n to be an indeterminate value after the setName function has been called [because the reference parameter is unconditionally turned to the right value, which will be empty after the assignment operation, i.e., the row parameter value is changed], if you want to overload it [overloaded functions referenced by left and right values] Resolve, there will be versions with infinite number of parameters [such as std::shared, overloaded form templae<typename T, typename... Args>shared_ptr<T> make_shared (Args &&, args)]; or run-time efficiency issues [overloaded versions will create temporary objects for row parameter binding, which will call a construct, a move assignment operation, a destructor] :
class Widget{ public: template<class T> void setName(T&& newName){ name = move(newName); } private: std::string name; std::shared_ptr<testWidget> p; }; std::string getWidgetName(); // factory function Widget w; auto n = getWidgetName(); // n is local variable w.setName(n); // moves n into w!
Overloaded version:
class Widget { public: void setName(const std::string& newName) // set from { name = newName; } // const lvalue void setName(std::string&& newName) // set from { name = std::move(newName); } // rvalue ... };
3.2 Last used std::forward and std::move
- To bind an object to a right or universal reference more than once within a single function and to ensure that its value is not moved until other operations on the object have been completed, you must implement std::move[or std::move_if_noexcept] and std::forward only the last time the reference is used.
For example:
template<typename T> void setSignText(T&& text){ sign.setText(text); auto now = std::chrono::system_clock::now(); signHistory.add(now, std::forward<T>(text)); }
3.3 Use std::forward and std::move for right-value and universal references to functions returned by value
- In a function returned by value, if the object returned is bound to a right-value reference or a universal reference, it is returned with std::forward or std::move.
In the following example, if Matrix supports a mobile construct, it will be more efficient than a copy construct. If Matrix does not support mobile constructs, it also copies itself through Matrix's copy constructor.
Matrix // by-value return operator+(Matrix&& lhs, const Matrix& rhs) { lhs += rhs; return std::move(lhs); // move lhs into } // return value
The same optimization does not apply to the local variable to be returned. Because C++ uses return value optimization (RVO), which creates a local variable w directly on the memory allocated for the function return value to avoid duplication. Use std::move or std:;forward may instead prevent the compiler from optimizing.
The conditions for RVO are as follows:
- The local object type is the same as the function return value type.
- What is returned is the local object itself.
When the preconditions for RVO permit, either std::move is implicitly implemented on the returned local object for them to be copied out.
4 Notes
4.1 Avoid overloading with universal references
Reason:
- Universal references absorb a large number of parameter types;
- For non-constant left-value types, it is generally better to match the copy constructor and hijack calls to the copy and move constructors of the base class in the derived class.
Enhance your experience with the examples below.
std::multiset<std::string> names; // global data structure #include <chrono> #include <ctime> void log(std::chrono::time_point<std::chrono::system_clock, std::chrono::duration<long long int, std::ratio<1, 1000000>>> time, std::string){ std::time_t tt = std::chrono::system_clock::to_time_t (time); std::cout << "today is: " << ctime(&tt); } template<typename T> void logAndAdd(T&& name) { auto now = std::chrono::system_clock::now(); log(now, "logAndAdd"); names.emplace(std::forward<T>(name)); } //Overloaded version std::string nameFromIdx(int idx){// return name if(idx > 3) return "f"; std::vector<std::string> v{"a","b","c","d"}; return v[idx]; } // corresponding to idx void logAndAdd(int idx) // new overload { auto now = std::chrono::system_clock::now(); log(now, "logAndAdd"); names.emplace(nameFromIdx(idx)); } //call std::string petName("Darla"); // as before logAndAdd(petName); // as before, these logAndAdd(std::string("Persephone")); // calls all invoke logAndAdd("Patty Dog"); // the T&& overload logAndAdd(2); // calls int overload short nameIdx; logAndAdd(nameIdx); // error!
In the example above, because ** overloads resolution rules, exact matching is better than promotion to match. ** So the short type calls the Universal Reference Version, and the constructor for std::string does not have a version with a parameter of type short, so it fails.
The following example does the same thing as the above method, but instead of calling the copy constructor, it actually calls the instantiated template constructor explicit Person (Person&n): name (std:: forward<Person &> (n){} because cloneOfP initializes a non-constant left value (p). And the following code cannot be compiled because there is no parameter type that matches the std::string constructor.
class Person { public: template<typename T> // perfect forwarding ctor explicit Person(T&& n) : name(std::forward<T>(n)) {}//error: no matching constructor for initialization of 'std::string' explicit Person(int idx){} // int ctor Person(const Person& rhs){} // copy ctor // (compiler-generated) Person(Person&& rhs){} // move ctor // (compiler-generated) private: std::string name; }; //call Person p("Nancy"); auto cloneOfP(p);//Unable to compile
If you want to call a copy constructor, you need to do the following. Although a template constructor can be instantiated to get the same signature as a copy constructor, since a template function and a non-function template have the same degree of matching, the regular function is preferred, so the copy constructor is called.
const Person cp("Nancy"); // object is now const auto cloneOfP(cp); // calls copy constructor
In the inheritance relationship below, instead of copying and moving constructs in the class, the instantiated template constructor is invoked due to parameter type matching. The code cannot be compiled because std::string does not have a constructor that accepts SpecialPerson.
class SpecialPerson: public Person{ public: SpecialPerson(const SpecialPerson& rhs): Person(rhs){} SpecialPerson(SpecialPerson&& rhs): Person(std::move(rhs)){} //Both of the above constructors call constructors of base classes };
41.1 Universal Reference Overload Alternative
- 1, overload with functions that do not use function names, but cannot be used with fixed-name functions such as constructors;
- 2, pass the left value constant reference type [const T&] instead of the universal reference;
- 3. Use value transfer instead of reference type for parameter type passed;
- As in the example above, use explicit Person(std::string n): name(std::move(n)) {} instead of T&Constructor
- 4. Use labels for call distribution so that overloads can be combined with universal references;
The sample code is as follows:
This is the original version:
template<typename T> void logAndAdd(T&& name) { auto now = std::chrono::system_clock::now(); log(now, "logAndAdd"); names.emplace(std::forward<T>(name)); }
The following is an improved version, in logAndAdd, the call is distributed according to the type of parameter, if it is std::string type, the logAndAddImpl that refers to the version of Universal is called, if it is int type, the logAndAddImpl of non-template type is called. For complete test code see appendix.
template<typename T> void logAndAdd(T&& name){ logAndAddImpl(std::forward<T>(name),std::is_integral<typename std::remove_reference<T>::type>()); } //Here are the two versions called template<typename T> void logAndAddImpl(T&& name, std::false_type){ auto now = std::chrono::system_clock::now(); log(now, "logAndAdd"); names.emplace(std::forward<T>(name)); } void logAndAddImpl(int idx, std::true_type){ logAndAdd(nameFromIdx(idx)); }
- 5, using std::enable_if limits templates to use universal references and overloads together;
Std::enable_ A rough example of if use is as follows:
class Person { public: template<typename T, typename = typename std::enable_if<condition>::type> explicit Person(T&& n); };
Now we've refined our restrictions, which are that template construction is only enabled if the parameter is not Person or inherits from Person type or the type is not reshaped. The condition is! Std::is_ Base_ Of <Person, typename std::decay<T>:: value &&! Typename std::is_ Integral<std::remove_ Reference <T>:: value.
Explanation:
- Std::is_ Base_ Of <T1, T2>: value: It can be judged that if T2 is derived from T1, then the value is true, and all types can be considered derived from themselves;
- Std::decay<T>::type can remove references to T and cv modifiers (const and volatile modifiers), or it can be used to force arrays and function types to pointer types;
The complete code is:
class Person{ public: template<typename T, typename = std::enable_if_t< !std::is_base_of<Person, std::decay_t<T>>::value && !std::is_integral<std::remove_reference_t<T>>::value >> explicit Person(T&& n):name(std::forward<T>(n)){ std::cout<<"Universal Reference Call"<<std::endl; static_assert( std::is_constructible<std::string, T>::value, "Paramter n can't be used to construct a std::string!" ); } explicit Person(int idx):name(nameFromIdx(idx)){} private: std::string name; }; //test Person p("Nancy"); auto cloneOfP(p);
In the example above, std::is_constructible can determine during compilation whether an object with one type can be constructed from an object (or objects) of another type (or multiple types). That is, when a client tries to construct an object of type Person from an object of type std::string that cannot be constructed, an error message is generated.
Summary:
1. Perfect rotation is more efficient (because temporary objects are avoided), but some types cannot be perfectly forwarded;
2. Perfect forwarding is not easy to use and will produce a large amount of error information which is difficult to detect when something goes wrong.
4.2 Avoid using mobile semantics
Although in most cases, using mobile operations instead of replication can be very efficient, the following situations are not appropriate for using mobile semantics.
4.2.1 No move operation
Cause: The object being moved on behalf of does not provide a move operation.
For example, data members or base classes prohibit mobile operations, and types do not provide display support for mobile.
class Base{ public: Base(){} Base(Base&&) = delete; }; class A: public Base{ }; int main(){ A a; A a2(a); }
4.2.2 Failed to move faster
Some containers do not have inexpensive mobile operations at all, and some do support inexpensive mobile operations but do not satisfy container elements due to additional conditions.
For example, the content data of an object of type std::array is stored directly within the object and does not point to its pointer, so the time consumed by the move and the copy operations are not very different. As an example, the time charges for both operations are close to 100ns.
class TestWidget{}; int main(){ auto start = std::chrono::system_clock::now(); std::array<TestWidget, 10000>aw1; //auto aw2 = aw1;//1000 ns auto aw2 = std::move(aw1); //1000 ns auto end = std::chrono::system_clock::now(); auto duration = std::chrono::duration<double, std::nano>(end - start); std::cout << duration.count() << " ns\n"; }
Because many std::string implementations employ small string optimization (SSO) [which reduces the need for dynamic memory allocation], small strings (such as no more than 15 characters) are stored in a buffer within the std::string object instead of using heap-allocated storage, resulting in a move operation that is no more than a copy operation block.
4.2.3 Move unavailable
Some containers in the standard library provide strong exception security, and underlying replication operations will only be replaced with mobile operations if they are known to not throw exceptions.
This results in the compiler still forcing a replication operation to be called only because the move operation does not have a noexcept declaration added.
4.3 Cases of Perfect Forwarding Failures
Meaning of perfect forwarding: not only the forwarding object, but also the forwarding type, left/right value, whether const or volation modifier is present, etc. That is, we will use universal references, such as the code below.
template<typename T> void fwd(T&& param){ func(std::forward<T>(param)); } template<typename... Ts> void fwd(Ts&&... params){ func(std::forward<Ts>(params)...); }
Perfect forward failure refers to the fact that the target and forward functions perform different results for the same argument. For example, the following code:
f(expression);//An operation was performed fwd(expression);//A different operation was performed
Conditions for perfect forwarding failure:
- 1. The compiler cannot derive results for one or more fwd parameters. [Causes code to fail to compile]
- 2. The compiler derives incorrect results for one or more FWD parameters. [Causes an instance of fwd's type derivation to fail compilation; or calls the wrong overloaded version]
4.3.1 Template Type Deduction Failed
When the argument type has brace initializers, perfect forwarding fails.
In the following example, fwd({1,2,3}) is declared as std::initializer_ A function template of type list passes a curly bracket initializer, that is, the parameter of fwd is not declared std::initializer_list, the compiler will prohibit deriving categories from the expression {1,2,3} during fwd calls, i.e.'non-inferential context'.
The solution is to declare a local variable with auto first, then pass the local variable to the forward function.
void func(const std::vector<int>& v){} template<typename T> void fwd(T&& param){ func(std::forward<T>(param)); } template<typename... Ts> void fwd(Ts&&... params){ func(std::forward<Ts>(params)...); } int main(){ func({1,2,3}); fwd({1,2,3});//error: no matching function for call to 'fwd' }
Solution:
auto il = {1,2,3}; fwd(il);
4.3.2 Derivation results are of the wrong type
- 1, express null pointer as 0 or NULL;
- The result of the derivation will be a pointer type that reshapes rather than passes arguments. The solution is to use nullptr
- 2, declared-only integer static const member variable
- The code below will work correctly, although MinVal is not stored, but if a pointer points to it, an error will be compiled because the pointer has no address to point to [because MinVal is not stored].
class TestWidget{ public: static const std::size_t MinVal = 20;//Make a statement }; //No definition given int main(){ std::vector<int> widgetData; widgetData.reserve(TestWidget::MinVal);
- The following code will fail because the fwd parameter is a universal reference and the reference is a pointer that can be picked up, causing it to fail to compile;
void func(std::size_t val){} template<typename T> void fwd(T&& param){ func(std::forward<T>(param)); } template<typename... Ts> void fwd(Ts&&... params){ func(std::forward<Ts>(params)...); } class TestWidget{ public: static const std::size_t MinVal = 20;//Make a statement }; //No definition given int main(){ func(TestWidget::MinVal); fwd(TestWidget::MinVal);//Compile Error
- The solution is to add a definition.
class TestWidget{ public: static const std::size_t MinVal = 20;//Make a statement }; const std::size_t TestWidget::MinVal;
- 3, name of template or overloaded function
- In the example below, the func call is okay [the f declaration tells the compiler which version of processVal to use], but the fwd call will fail because the compiler does not know which version of processVal to match.
int processVal(int value){return value;} int processVal(int value, int priority){return value + priority;} template<typename T> void fwd(T&& param){ func(std::forward<T>(param)); } template<typename... Ts> void fwd(Ts&&... params){ func(std::forward<Ts>(params)...); } int main(){ func(processVal); fwd(processVal);// error: no matching function for call to 'fwd' }
- The same error occurs when function templates are used instead of overloaded function names.
template<typename T> T workOneval(T param){return param;} int main(){ fwd(workOneval); }
- The solution is to manually indicate which version of the overloaded version or instance to forward. For example, you can create a function pointer of the same type, then initialize that pointer with the overloaded version of the function you want to use, and pass the pointer to Perfect Forwarding. See below for an example:
using processValType = int(*)(int); processValType processValPtr = processVal; fwd(processValPtr); // fwd(processVal);// error: no matching function for call to 'fwd' fwd(static_cast<processValType>(workOneval)); // fwd(workOneval);
- 4, Bit Domain
- In the following example, func works fine, but the fwd function fails because a non-const reference cannot be bound to a bit field. Because a bit field is made up of machine bytes, such an entity cannot address it directly. [On the hardware level, there is no way to create a pointer to any bit, and the C++ hard rule states that the single smallest entity that can be pointed to is a char]
void func(std::size_t val){} template<typename T> void fwd(T&& param){ func(std::forward<T>(param)); } template<typename... Ts> void fwd(Ts&&... params){ func(std::forward<Ts>(params)...); } struct IPv4Header { std::uint32_t version:4, IHL:4, DSCP:6, ECN:2, totalLength:16; }; int main(){ IPv4Header h; func(h.totalLength); fwd(h.totalLength);//error: non-const reference cannot bind to bit-field 'totalLength' }
- Since any function that receives a bit field argument accepts only a copy of the bit field value, the only type of parameter that passes a bit field is by value. The solution, therefore, is to make your own copy and call the forwarding function from that copy. See below for an example.
auto length = static_cast<std::uint16_t>(h.totalLength); fwd(length);
5 Distinguish right value reference from universal reference
Generally speaking, universal reference refers to the distinction between left and right values in the process of type derivation, as well as right-value references in the context where folded references occur.
10 Appendix
10.1 Code 1
template<typename T> void logAndAdd(T&& name); #include <chrono> #include <ctime> void log(std::chrono::time_point<std::chrono::system_clock, std::chrono::duration<long long int, std::ratio<1, 1000000>>> time, std::string){ std::time_t tt = std::chrono::system_clock::to_time_t (time); std::cout << "today is: " << ctime(&tt); } std::string nameFromIdx(int idx){// return name if(idx > 3) return "f"; std::vector<std::string> v{"a","b","c","d"}; return v[idx]; } template<typename T> void logAndAddImpl(T&& name, std::false_type){ auto now = std::chrono::system_clock::now(); log(now, "logAndAdd"); names.emplace(std::forward<T>(name)); } void logAndAddImpl(int idx, std::true_type){ logAndAdd(nameFromIdx(idx)); } template<typename T> void logAndAdd(T&& name){ logAndAddImpl(std::forward<T>(name),std::is_integral<typename std::remove_reference<T>::type>()); }