在下一個測驗代碼中,我們有一個簡單的類MyClass
,只有一個變數成員 ( int myValue
) 和一個MyClass getChild()
回傳 的新實體的函式 ( ) MyClass
。此類具有多載的主要運算子以在呼叫它們時進行列印。
我們有三個帶有兩個引數的函式,它們執行簡單的賦值(first_param = second_param)::
func1
: 第二個引數是一個右值參考(也使用 astd::forward
)
void func1(MyClass &el, MyClass &&c) {
el = std::forward<MyClass>(c);
}
func2
: 第二個引數是一個const 左值參考
void func2(MyClass &el, const MyClass &c) {
el = c;
}
func3
: 兩個多載(一個等價于func1
,另一個等價于func2
)
void func3(MyClass &el, MyClass &&c) {
el = std::forward<MyClass>(c);
}
void func3(MyClass &el, const MyClass &c) {
el = c;
}
在main()
函式中,我們將這三個函式各呼叫 3 次,一個傳遞一個rvalue,另一個傳遞一個lvalue,另一個傳遞std::move(lvalue)
(或相同的,一個rvalue 參考)。在呼叫這些函式之前,我們還對左值、右值和右值參考進行了直接賦值(不呼叫任何函式)。
測驗代碼:
#include <iostream>
#include <utility>
class MyClass {
public:
int myValue;
MyClass(int n) { // custom constructor
std::cout << "MyClass(int n) [custom constructor]" << std::endl;
}
MyClass() { // default constructor
std::cout << "MyClass() [default constructor]" << std::endl;
}
~MyClass() { // destructor
std::cout << "~MyClass() [destructor]" << std::endl;
}
MyClass(const MyClass& other) // copy constructor
: myValue(other.myValue)
{
std::cout << "MyClass(const MyClass& other) [copy constructor]" << std::endl;
}
MyClass(MyClass&& other) noexcept // move constructor
: myValue(other.myValue)
{
std::cout << "MyClass(MyClass&& other) [move constructor]" << std::endl;
}
MyClass& operator=(const MyClass& other) { // copy assignment
myValue = other.myValue;
std::cout << "MyClass& operator=(const MyClass& other) [copy assignment]" << std::endl;
return *this;
}
MyClass& operator=(MyClass&& other) noexcept { // move assignment
myValue = other.myValue;
std::cout << "MyClass& operator=(MyClass&& other) [move assignment]" << std::endl;
return *this;
}
MyClass getChild() const {
return MyClass(myValue 1);
}
};
void func1(MyClass &el, MyClass &&c) {
el = std::forward<MyClass>(c);
}
void func2(MyClass &el, const MyClass &c) {
el = c;
}
void func3(MyClass &el, MyClass &&c) {
el = std::forward<MyClass>(c);
}
void func3(MyClass &el, const MyClass &c) {
el = c;
}
int main(int argc, char** argv) {
MyClass root(200);
MyClass ch = root.getChild();
MyClass result;
std::cout << "==================================================================" << std::endl;
std::cout << "------------- simple assignment to rvalue ------------------------" << std::endl;
result = root.getChild();
std::cout << "------------- simple assignment to lvalue ------------------------" << std::endl;
result = ch;
std::cout << "------------- simple assignment to std::move(lvalue) -------------" << std::endl;
result = std::move(ch);
std::cout << "==================================================================" << std::endl;
std::cout << "------------- func1 with rvalue ----------------------------------" << std::endl;
func1(result, root.getChild());
std::cout << "------------- func1 with lvalue ----------------------------------" << std::endl;
//func1(result, ch); // does not compile
std::cout << "** Compiler error **" << std::endl;
std::cout << "------------- func1 with std::move(lvalue) -----------------------" << std::endl;
func1(result, std::move(ch));
std::cout << "==================================================================" << std::endl;
std::cout << "------------- func2 with rvalue ----------------------------------" << std::endl;
func2(result, root.getChild());
std::cout << "------------- func2 with lvalue ----------------------------------" << std::endl;
func2(result, ch);
std::cout << "------------- func2 with std::move(lvalue) -----------------------" << std::endl;
func2(result, std::move(ch));
std::cout << "==================================================================" << std::endl;
std::cout << "------------- func3 with rvalue ----------------------------------" << std::endl;
func3(result, root.getChild());
std::cout << "------------- func3 with lvalue ----------------------------------" << std::endl;
func3(result, ch);
std::cout << "------------- func3 with std::move(lvalue) -----------------------" << std::endl;
func3(result, std::move(ch));
std::cout << "==================================================================" << std::endl;
return 0;
}
使用 g 編譯后(使用 -O0 或 -O3 無關緊要)并運行它,結果是:
MyClass(int n) [custom constructor]
MyClass(int n) [custom constructor]
MyClass() [default constructor]
==================================================================
------------- simple assignment to rvalue ------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- simple assignment to lvalue ------------------------
MyClass& operator=(const MyClass& other) [copy assignment]
------------- simple assignment to std::move(lvalue) -------------
MyClass& operator=(MyClass&& other) [move assignment]
==================================================================
------------- func1 with rvalue ----------------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func1 with lvalue ----------------------------------
** Compiler error **
------------- func1 with std::move(lvalue) -----------------------
MyClass& operator=(MyClass&& other) [move assignment]
==================================================================
------------- func2 with rvalue ----------------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(const MyClass& other) [copy assignment]
~MyClass() [destructor]
------------- func2 with lvalue ----------------------------------
MyClass& operator=(const MyClass& other) [copy assignment]
------------- func2 with std::move(lvalue) -----------------------
MyClass& operator=(const MyClass& other) [copy assignment]
==================================================================
------------- func3 with rvalue ----------------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func3 with lvalue ----------------------------------
MyClass& operator=(const MyClass& other) [copy assignment]
------------- func3 with std::move(lvalue) -----------------------
MyClass& operator=(MyClass&& other) [move assignment]
==================================================================
~MyClass() [destructor]
~MyClass() [destructor]
~MyClass() [destructor]
For the assignment, the result is as expected. If you pass an rvalue, it calls to move assignment, if you pass an lvalue, it calls to copy assignment, and if you pass an rvalue reference (std::move(lvalue)
) it calls to a move assignment.
The calls to func1
are also the expected (remember that this function receives an rvalue reference). If you pass an rvalue, it calls to move assignment, if you pass an lvalue, the compilation fails (because an lvalue can't bind to rvalue reference), and if you pass an rvalue reference (std::move(lvalue)
) it calls to a move assignment.
But for func2
, in the three cases, the copy assignment is called. This function receives as a second parameter a const lvalue reference, this is an lvalue and then it calls to copy assignment. I understand this, but, why the compiler does not optimize this function when it is called with a temporal object (rvalue or rvalue reference) calling the move assignment operator instead of the copy assignment?
The func3
is an attempt to create a function that works in the same way as a direct assignment, combining the func1
behaviour and defining an overload with func2
behaviour for when an lvalue is passed. This works, but this solution requires the function code to be duplicated into the two functions (not exactly, since in one solution we have to use std::forward
). Is there a way to achieve this by avoiding having to duplicate the code? This function is small but could be larger in other contexts.
In summary, there are two questions:
Why is the func2
function not optimized to call the move assignment when it receives an rvalue or an rvalue reference?
How could I modify the func3
function so as not to have to "duplicate" the code?
EDIT to clarify my reflections after Brian's answer.
I understand the first point (on why the compiler does not optimize this). It is simply how it works by definition of the language, and the compiler cannot optimize this simply because which operators must be called on each occasion are well defined and must be respected. The programmer expects certain operators to be called and an optimization attempt would unpredictably change which and how they would be called. The only exceptions I have come across are Return Value Optimization (RVO), where the compiler can eliminate the temporary object created to hold the return value of a function; and the cases where Copy Elision can be applied to eliminate unnecessary copying of objects. According to its wikipedia article, the optimization can not be applied to a temporary object that has been bound to a reference (I think this is exactly what applies to our case):
Another widely implemented optimization, described in the C standard, is when a temporary object of class type is copied to an object of the same type. As a result, copy-initialization is usually equivalent to direct-initialization in terms of performance, but not in semantics; copy-initialization still requires an accessible copy constructor. The optimization can not be applied to a temporary object that has been bound to a reference.
On avoiding duplicate code, I have tried the solutions in the suggested SO posts ([1], [2]), which may be convenient on some occasions but they do not serve as an exact replacement for the solution with the duplicated code (func3
), since they will work fine when an rvalue or rvalue reference is passed to the function, but they do not work exactly as expected when passing an lvalue.
To test this, taking into account the original code, we add two functions func4
and func5
to implement the proposed solutions:
template<typename T>
inline constexpr void func4(T &el, T &&c) {
el = std::forward<T>(c);
}
template<typename T>
inline constexpr void func4(T &el, const T &c) {
T copy = c;
func4(el, std::move(copy));
}
template<class T>
std::decay_t<T> copy(T&& t) {
return std::forward<T>(t);
}
template<typename T>
inline constexpr void func5(T &el, T &&c) {
el = std::forward<T>(c);
}
template<typename T>
inline constexpr void func5(T &el, const T &c) {
func5(el, copy(c));
}
As with the original functions, we call these functions with an rvalue, an lvalue and an rvalue reference (std::move(lvalue)
), the result is the following:
==================================================================
------------- func4 with rvalue ----------------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func4 with lvalue ----------------------------------
MyClass(const MyClass& other) [copy constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func4 with std::move(lvalue) -----------------------
MyClass& operator=(MyClass&& other) [move assignment]
==================================================================
------------- func5 with rvalue ----------------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func5 with lvalue ----------------------------------
MyClass(const MyClass& other) [copy constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func5 with std::move(lvalue) -----------------------
MyClass& operator=(MyClass&& other) [move assignment]
==================================================================
In the case of lvalue, instead of calling the copy assignment directly, a temporary object will be created by calling the copy constructor operator, and then the move assignment operator is called; which is more inefficient than just calling copy assignment operator without creating a temporal object (which is what func3
did by duplicating code).
From what I understand, at the moment there is no totally equivalent method to avoid code duplication.
uj5u.com熱心網友回復:
考慮以下示例:
void foo(int) {}
void foo(double) {}
void bar(double x) {
foo(x);
}
int main() {
bar(0);
}
在上面的程式中,foo(double)
將始終被呼叫,而不是foo(int)
. 這是因為雖然引數最初是 an int
,但是一旦您進入內部,這些資訊就無關緊要了bar
。bar
只看到它自己的引數x
,double
無論原始引數型別是什么,它都有型別。因此,它呼叫foo
與引數型別最佳匹配的多載x
。
你的func2
作品類似:
void func2(MyClass &el, const MyClass &c) {
el = c;
}
這里,運算式c
是一個左值,即使在呼叫時參考可能已經系結到一個臨時物件。因此,編譯器必須選擇將=
左值作為其右引數的運算子。
為了將左值作為左值轉發,將右值作為右值轉發,經常使用const MyClass&
和多載MyClass&&
,即使(如您所見)它是重復的。有關如何減少代碼重復的一些建議,請參閱是否有辦法在提供左值和右值多載時洗掉重復代碼?以及如何防止右值和左值成員函式之間的代碼重復?
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/368074.html
標籤:c c 11 optimization rvalue-reference lvalue
下一篇:如何直接從輸入流中插入值?