std::move,std::forward與左右值引用
在講這個之前, 必須得講一下左值和右值, 這個知識真的是很冷門又冷門, 如果不是C++11的std::move, 我想我八輩子都不會知道這是什麼東西, 晦澀難懂
左值
簡單的來說就是賦值符號左邊的值, 準確的來說, 左值就是表示式執行後依然存在的持久的物件
右值
右邊的值,表示式執行過後就不再存在的臨時物件, 但是C++肯定是要把程式設計搞得更復雜的, 所以右值又有純右值, 和將亡值
純右值: 純粹的右值要麼是純粹的的字面量, 比如2233
, true
;要麼是求值結果相當於字面量或者是匿名臨時物件, 比如一個加法2*3
,非引用返回的臨時變數, 運算表示式的臨時變數, 原始字面量,lambda表示式都是純右值
將亡值: 就是可以被移動並且將要被銷燬的值. 這個說得有點抽象, 看個程式碼
std::vector<int> foo(){
std::vector<int> temp={1,2,3,4};
return temp;
}
int main(int argc,char *argv[]){
auto res=foo();
return 0;
}
現在這個寫法相信大家肯定都看過了, 但是這個存在一個問題, foo的返回值在這個裡面作為一個臨時的物件被返回, 這個賦值語句做了什麼呢, 他把這個臨時物件拷貝一遍, 這個返回的物件或者值事實上就是一個右值而且是個純右值, 因為他是表示式求值後得到的值, 如果這個foo()
Rvalue references are a new reference type introduced in C++0x that help solve the problem of unncessatry copying and enable perfect forwarding. When the right-hand saide of an assignment is an rvalue, then the left-side object can steal resources from the right-hand side object rather than proforming a separate allocation, thus enabling move semantics.
我的翻譯是這樣的:
右值引用是一種新型的引用, 在C++0x十分被推薦使用, 它用來解決一個問題, 這個問題是關於如何避免不必要的拷貝和更好的進行forward, 當一個右值需要賦值到左值的時候, 左值可以去“竊取”右值的資源, 而且還並不需要開闢一個新的空間,
我們大概理解了這個東西, 大概就是為了防止比如vector insert的時候去拷貝臨時物件, 而是去指向他的引用, 這個是個好東西, 可以防止吃記憶體, 我們也可以大膽猜測, 事實上右值引用就是指標的淺拷貝.
但是關於我找到了這樣的兩個程式碼, 來看這個程式碼:
int main() {
std::string s1("Hello");
std::string s2("World");
s1+s2=s2;
std::cout<<"s1:"<<s1<<std::endl;
std::cout<<"s2:"<<s2<<std::endl;
std::string()="World";
}
超屌的, 這段程式碼居然是可以編譯的, 而且還可以執行, s1+s2
很明顯他是一個右值, 但是卻被放在了左邊, std::string()
是個臨時物件竟然可以賦值, 類似的程式碼還有
void complex_test(){
std::complex<int> c1(3,8),c2(1,0);
c1+c2=std::complex<int>(6,9);
std::cout<<c1<<" "<<c2<<std::endl;
std::complex<int>() =std::complex<int> (59,1);
}
這似乎是標準庫的bug, 但是這樣做確實是不被允許的
現在有了右值我們就有了另一種程式碼, 我們在向某個函式傳入臨時物件的時候, 如果這個物件後面不會再使用我們就可以傳入他的右值引用, 這個過程不回去呼叫它的拷貝建構函式,而是呼叫他的引用建構函式,
//現在我們有兩套拷貝的建構函式
//這是vector的iterator
iterator
insert(const_iterator __position,const value_type& __x);
iterator(const_iterator __position,value_type&& __x){
return emplace(__ position,std::move(__x));
}
那麼, 我有什麼辦法來進行使用呢?
std::move
#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main()
{
std::string str = "Hello";
std::vector<std::string> v;
//呼叫常規的拷貝建構函式,新建字元陣列,拷貝資料
v.insert(v.end(),str);
std::cout << "After copy, str is \"" << str << "\"\n";
//呼叫移動建構函式,掏空str,掏空後,最好不要使用str
v.insert(v.end(),std::move(str));
std::cout << "After move, str is \"" << str << "\"\n";
std::cout << "The contents of the vector are \"" << v[0]
<< "\", \"" << v[1] << "\"\n";
}
這裡用到了std::move, 在C++11後就有了這個東西, 它可以把左值強制轉化為右值, 你可以解釋它為一個cast, 以前我們如果要在C++裡面讓一個指標搬家, 我們需要先把它拷貝, 再把它清空,讓它指向空, 現在不用了, 現在你可以移動(竊取)他.
所以我們有了
左值引用和右值引用
直接看程式碼
#include <iostream>
#include <string>
void reference(std::string& str) {
std::cout << "左值" << std::endl;
}
void reference(std::string&& str) {
std::cout << "右值" << std::endl;
}
int main()
{
std::string lv1 = "string,"; // lv1 是一個左值
// std::string&& r1 = s1; // 非法, 右值引用不能引用左值
std::string&& rv1 = std::move(lv1); // 合法, std::move可以將左值轉移為右值
std::cout << rv1 << std::endl; // string,
const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能夠延長臨時變數的申明週期
// lv2 += "Test"; // 非法, 引用的右值無法被修改
std::cout << lv2 << std::endl; // string,string
std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延長臨時物件宣告週期
rv2 += "Test"; // 合法, 非常量引用能夠修改臨時變數
std::cout << rv2 << std::endl; // string,string,string,
reference(rv2); // 輸出左值
return 0;
}
完美轉發 std::forward
如果我們宣告一個右值引用, 那麼這個右值引用本生其實上是一個左值對吧, 看程式碼
void reference(int& v) {
std::cout << "左值" << std::endl;
}
void reference(int&& v) {
std::cout << "右值" << std::endl;
}
template <typename T>
void pass(T&& v) {
std::cout << "普通傳參:";
reference(v); // 始終呼叫 reference(int& )
}
int main() {
std::cout << "傳遞右值:" << std::endl;
pass(1); // 1是右值, 但輸出左值
std::cout << "傳遞左值:" << std::endl;
int v = 1;
pass(v); // r 是左引用, 輸出左值, 因為編譯器幫我們優化了這一步,上面並不是一個右值的引用, 而是一個引用的引用
return 0;
}
對於 pass(1) 來說,雖然傳遞的是右值,但由於 v 是一個引用,所以同時也是左值。因此 reference(v) 會呼叫 reference(int&),輸出『左值』。而對於pass(v)而言,v是一個左值,為什麼會成功傳遞給 pass(T&&) 呢?
其實上述的程式碼都還有問題, 那就是事實上如果不用模版函式還會有錯誤
void check_lr(int &value){
std::cout<<"This is a lvalue"<<std::endl;
}
void check_lr(int &&value){
std::cout<<"This is a rvalue"<<std::endl;
}
void do_check(int&& value){
check_lr(value);
}
int main() {
int x=520;
check_lr(x); //binggo
check_lr(1); //bingo
check_lr(std::move(x));//bingo
do_check(520); //為什麼到了check裡面520變成了一個左值,(事實上他變成了一個named object),這個叫imperfect forward!
do_check(std::move(x));//為什麼你也成了左值
//do_check(x); //編譯都過不了 一個右值引用的形參怎麼可以接受一個左值呢
//所以上面那個template的pass是編譯器替我們做的優化, 進行了型別的推導, 儘可能的可以編譯, 它強行把我們傳入的v作為一個左值的引用,這個叫引用坍縮規則
return 0;
}
引用坍縮規則
這是基於引用坍縮規則的:在傳統 C++ 中,我們不能夠對一個引用型別繼續進行引用,但 C++ 由於右值引用的出現而放寬了這一做法,從而產生了引用坍縮規則,允許我們對引用進行引用,既能左引用,又能右引用。但是卻遵循如下規則:
因此,模板函式中使用 T&& 不一定能進行右值引用,當傳入左值時,此函式的引用將被推導為左值。更準確的講,無論模板引數是什麼型別的引用,當且僅當實參型別為右引用時,模板引數才能被推導為右引用型別。這才使得 v 作為左值的成功傳遞。
完美轉發就是基於上述規律產生的。所謂完美轉發,就是為了讓我們在傳遞引數的時候,保持原來的引數型別(左引用保持左引用,右引用保持右引用)。為了解決這個問題,我們應該使用 std::forward 來進行引數的轉發(傳遞):
#include <iostream>
#include <utility>
void reference(int& v) {
std::cout << "左值引用" << std::endl;
}
void reference(int&& v) {
std::cout << "右值引用" << std::endl;
}
template <typename T>
void pass(T&& v) {
std::cout << "普通傳參:";
reference(v);
std::cout << "std::move 傳參:";
reference(std::move(v));
std::cout << "std::forward 傳參:";
reference(std::forward<T>(v));
}
int main() {
std::cout << "傳遞右值:" << std::endl;
pass(1);
std::cout << "傳遞左值:" << std::endl;
int v = 1;
pass(v);
return 0;
}