引用
計算機專業(yè)大一的基礎(chǔ)內(nèi)容之一,值傳遞和引用傳遞:
void foo(int& a)
{
a = 2;
}
void bar(int a)
{
a = 3;
}
int main()
{
int a = 1;
foo(a);
cout << a << endl;
bar(a);
cout << a << endl;
return 0;
}
這里會輸出兩個2:
2
2
當然用指針可以達到類似的效果古沥,但不完全等價:
void foo(int* a)
{
*(a) = 2;
}
void bar(int a)
{
a = 3;
}
int main()
{
int a = 1;
foo(&a);
cout << a << endl;
bar(a);
cout << a << endl;
return 0;
}
用指針太容易出現(xiàn)內(nèi)存問題了娇跟,引用更加安全,指針可能會出現(xiàn)這樣的情況:
void foo(int* a)
{
int b = 0;
a = &b;
*(a) = 2; //改變了a指向的地址盹沈,所以修改不對main中的a起效
}
void bar(int a)
{
a = 3;
}
int main()
{
int a = 1;
foo(&a);
cout << a << endl;
bar(a);
cout << a << endl;
return 0;
}
左值引用與右值引用
眾所周知C++允許程序員非常自由地操作內(nèi)存吃谣,為了更加靈活地使用引用,C++11添加了右值引用的特性肃晚。
試想如下場景:
void foo(int a)
{
bar(a);
}
void bar(const int a)
{
cout << a << endl;
}
int main()
{
int a = 2;
foo(a);
foo(1);
}
這會導(dǎo)致foo和bar函數(shù)每次都在棧內(nèi)存上新建一個變量仔戈,于是我們加入引用以節(jié)省內(nèi)存:
void foo(int& a)
{
bar(a);
}
void bar(const int& a)
{
cout << a << endl;
}
int main()
{
int a = 2;
foo(a);
foo(1);
}
這會導(dǎo)致編譯錯誤,因為參數(shù)int& a是一個左值晋修,而常量1是一個右值凰盔,通過修改int&為const int&可以部分解決這個問題:
void foo(const int& a) // const & binds to everything
{
bar(a);
}
void bar(const int& a)
{
cout << a << endl;
}
int main()
{
int a = 2;
foo(a);
foo(1);
}
但是這樣在foo中就無法修改a的值,不夠靈活落剪,于是我們加入右值引用的foo實現(xiàn):
void bar(const int& a)
{
cout << a << endl;
}
void foo(int&& a)
{
cout << "rvalue" << endl;
bar(a);
}
void foo(int& a)
{
cout << "lvalue" << endl;
bar(a);
}
int main()
{
int a = 2;
foo(a);
foo(1);
}
看似完美解決了問題尿庐,但是這是單個參數(shù)的情況,如果是2個參數(shù),那么就要重載4個函數(shù)暮胧,3個參數(shù)就要重載8個函數(shù)……
于是有了萬能引用:
void bar(const int& a)
{
cout << a << endl;
}
template<typename T>
void foo(T&& a)
{
bar(a);
}
int main()
{
int a = 2;
foo(a);
foo(1);
}
似乎一切正常,但是僅僅這樣是不夠的钞翔,如果我們重載一下bar,就會發(fā)現(xiàn)哮笆,foo把左值和右值引用都轉(zhuǎn)化成了左值:
void bar(int& a)
{
cout << "lvalue" << endl;
}
void bar(int&& a)
{
cout << "rvalue" << endl;
}
template<typename T>
void foo(T&& a)
{
// a here is always a left reference
bar(a);
}
int main()
{
int a = 2;
foo(a);
foo(1);
}
由于自動類型推導(dǎo)(常數(shù)1被推導(dǎo)為int型)汰扭,這將輸出兩個lvalue,顯然不符合我們的預(yù)期项阴,此時笆包,搭配std::forward才能正確地實現(xiàn)完美轉(zhuǎn)發(fā):
void bar(int& a)
{
cout << "lvalue" << endl;
}
void bar(int&& a)
{
cout << "rvalue" << endl;
}
template<typename T>
void foo(T&& a)
{
bar(forward<T>(a));
}
int main()
{
int a = 2;
foo(a);
foo(1);
}
這將輸出:
lvalue
rvalue
引用折疊
上述內(nèi)容還涉及一個問題,引用的引用如何定義歉胶?
對于如下萬能引用:
template<typename T>
void foo(T&& a);
當T的類型為一個int&時巴粪,a的類型事實上是int& &&,也就是一個左值引用的右值引用衡创,但是C++不允許類似int& &&這樣的類型聲明晶通,它要么是int&要么是int&&,于是就有了引用折疊:
int& && -> int&
int& & -> int&
int&& & -> int&
int&& && -> int&&
可以看到左值引用具有傳染性一也,類似一個或門喉脖,僅有右值引用的右值引用折疊結(jié)果為右值引用,這也是std::forward的實現(xiàn)原理:
template<typename T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
}
template<typename T>
T&& forward(typename std::remove_reference<T>::type&& t) noexcept {
static_assert(!std::is_lvalue_reference<T>::value, "Invalid argument: cannot forward an lvalue as an rvalue.");
return static_cast<T&&>(t);
}
可以看到舆蝴,std::forward通過std::remove_reference去除了t的引用限定符,也就是不管類型T加了多少引用限定符层皱,都回歸到基本類型赠潦,比如一個int& &&,經(jīng)過std::remove_reference她奥,會得到一個int類型,通過重載绷跑,自動調(diào)用左值或右值版本携茂,并在最后用static_cast<T&&>返回一個t的右值引用弛姜,又由于引用折疊,t如果是一個左值引用扇售,比如int&膝藕,那么返回的就是int& &&咐扭,最終還是一個int&,同理蝗肪,int&& &&返回的就是一個int&&薛闪。
相關(guān)話題:std::move
設(shè)想如下代碼:
void bar(int& a)
{
cout << "lvalue" << endl;
}
void bar(int&& a)
{
cout << "rvalue" << endl;
}
template<typename T>
void foo(T&& a)
{
bar(forward<T>(a));
}
void test(int& a)
{
cout << "ltest" << endl;
foo(a);
}
void test(int&& a)
{
cout << "rtest" << endl;
foo(a);
}
int main()
{
int a = 2;
test(a);
test(2);
}
在執(zhí)行test函數(shù)時,可以正常重載對應(yīng)類型的函數(shù)昙篙,但是test調(diào)用foo時诱咏,由于自動類型推導(dǎo),兩次都是lvalue袋狞,輸出為:
ltest
lvalue
rtest
lvalue
使用std::move,可以將變量顯式轉(zhuǎn)換為右值引用:
void bar(int& a)
{
cout << "lvalue" << endl;
}
void bar(int&& a)
{
cout << "rvalue" << endl;
}
template<typename T>
void foo(T&& a)
{
bar(forward<T>(a));
}
void test(int& a)
{
cout << "ltest" << endl;
foo(a);
}
void test(int&& a)
{
cout << "rtest" << endl;
foo(move(a)); // same as std::forward<int&&>(a) here
}
int main()
{
int a = 2;
test(a);
test(2);
}
這將輸出:
ltest
lvalue
rtest
rvalue
如上場景使用move實現(xiàn)了類似forward的功能秧荆,兩者又是有區(qū)別的埃仪,forward只是保持來源的引用類型不變陕赃,而move可以將左值轉(zhuǎn)變?yōu)橛抑担热缦胍{(diào)用移動構(gòu)造函數(shù)而非拷貝構(gòu)造函數(shù)來節(jié)約內(nèi)存時么库,就可以通過move實現(xiàn)诉儒。