## 文章首發 [【重學C++】05 | 說透右值引用、移動語義、完美轉發(下)](https://mp.weixin.qq.com/s/w7yXp6efE7_V0EHxXWJiJA) ## 引言 大家好,我是只講技術乾貨的會玩code,今天是【重學C++】的第五講,在第四講《[【重學C++】04 ...
文章首發
【重學C++】05 | 說透右值引用、移動語義、完美轉發(下)
引言
大家好,我是只講技術乾貨的會玩code,今天是【重學C++】的第五講,在第四講《【重學C++】04 | 說透右值引用、移動語義、完美轉發(上)》中,我們解釋了右值和右值引用的相關概念,並介紹了C++的移動語義以及如何通過右值引用實現移動語義。今天,我們聊聊右值引用的另一大作用 -- 完美轉發。
什麼是完美轉發
假設我們要寫一個工廠函數,該工廠函數負責創建一個對象,並返回該對象的智能指針。
template<typename T, typename Arg>
std::shared_ptr<T> factory_v1(Arg arg)
{
return std::shared_ptr<T>(new T(arg));
}
class X1 {
public:
int* i_p;
X(int a) {
i_p = new int(a);
}
}
對於類X
的調用方來說,auto x1_ptr = factory_v1<X1>(5);
應該與auto x1_ptr = std::shared_ptr<X>(new X1(5))
是完全一樣的。
也就是說,工廠函數factory_v1
對調用者是透明的。要達到這個目的有兩個前提:
- 傳給
factory_v1
的入參arg
能夠完完整整(包括引用屬性、const屬性等)得傳給T
的構造函數。 - 工廠函數
factory_v1
沒有額外的副作用。
這個就是C++的完美轉發。
單看factory_v1
應用到X1
貌似很"完美",但既然是工廠函數,就不能只滿足於一種類對象的應用。假設我們有類X2
。定義如下
class X2 {
public:
X2(){}
X2(X2& rhs) {
std::cout << "copy constructor call" << std::endl;
}
}
現在大家再思考下麵代碼:
X2 x2 = X2();
auto x2_ptr1 = factory_v1<X2>(x2);
// output:
// copy constructor call
// copy constructor call
auto x2_ptr2 = std::shared_ptr<X2>(x2)
// output:
// copy constructor call
可以發現,auto x2_ptr1 = factory_v1<X2>(x2);
比 auto x2_ptr2 = std::shared_ptr<X2>(x2)
多了一次拷貝構造函數的調用。
為什麼呢?很簡單,因為factory_v1
的入參是值傳遞,所以x2
在傳入factory_v1
時,會調用一次拷貝構造函數,創建arg
。很直接的辦法,把factory_v1
的入參改成引用傳遞就好了,得到factory_v2
。
template<typename T, typename Arg>
std::shared_ptr<T> factory_v2(Arg& arg)
{
return std::shared_ptr<T>(new T(arg));
}
改成引用傳遞後,auto x1_ptr = factory_v2<X1>(5);
又會報錯了。因為factory_v2
需要傳入一個左值,但字面量5
是一個右值。
方法總比困難多,我們知道,C++的const X&
類型參數,既能接收左值,又能接收右值,所以,稍加改造,得到factory_v3
。
template<typename T, typename Arg>
std::shared_ptr<T> factory_v3(const Arg& arg)
{
return std::shared_ptr<T>(new T(arg));
}
factory_v3
還是不夠"完美", 再看看另外一個類X3
。
class X3 {
public:
X3(){}
X3(X3& rhs) {
std::cout << "copy constructor call" << std::endl;
}
X3(X3&& rhs) {
std::cout << "move constructor call" << std::endl;
}
}
再看看以下使用例子
auto x3_ptr1 = factory_v3<X3>(X3());
// output
// copy constructor call
auto x3_ptr2 = std::shared_ptr<X3>(new X3(X3()));
// output
// move constructor call
通過上一節我們知道,有名字的都是左值,所以factory_v3
永遠無法調用到T
的移動構造函數。所以,factory_v3
還是不滿足完美轉發。
特殊的類型推導 - 萬能引用
給出完美轉發的解決方案前,我們先來瞭解下C++中一種比較特殊的模版類型推導規則 - 萬能引用。
// 模版函數簽名
template <typename T>
void foo(ParamType param);
// 應用
foo(expr);
模版類型推導是指根據調用時傳入的expr
,推導出模版函數foo
中ParamType
和param
的類型。
類型推導的規則有很多,大家感興趣可以去看看《Effective C++》[1],這裡,我們只介紹一種比較特殊的萬能引用。 萬能引用的模版函數格式如下:
template<typename T>
void foo(T&& param);
萬能引用的
ParamType
是T&&
,既不能是const T&&
,也不能是std::vector<T>&&
萬能引用的規則有三條:
- 如果
expr
是左值,T
和param
都會被推導成左值引用。 - 如果
expr
是右值,T
會被推導成對應的原始類型,param
會被推導成右值引用(註意,雖然被推導成右值引用,但由於param
有名字,所以本身還是個左值)。 - 在推導過程中,
expr
的const屬性會被保留下來。
看下麵示例
template<typename T>
void foo(T&& param);
// x是一個左值
int x=27;
// cx是帶有const的左值
const int cx = x;
// rx是一個左值引用
const int& rx = cx;
// x是左值,所以T是int&,param類型也是int&
foo(x);
// cx是左值,所以T是const int&,param類型也是const int&
foo(cx);
// rx是左值,所以T是const int&,param類型也是const int&
foo(rx);
// 27是右值,所以T是int,param類型就是int&&
foo(27);
std::forward實現完美轉發
到此,完美轉發的前置知識就已經講完了,我們看看C++是如何利用std::forward
實現完美轉發的。
template<typename T, typename Arg>
std::shared_ptr<T> factory_v4(Arg&& arg)
{
return std::shared_ptr<T>(new T(std::forward<Arg>(arg)));
}
std::forward
的定義如下
template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
{
return static_cast<S&&>(a);
}
傳入左值
X x;
auto a = factory_v4<A>(x);
根據萬能引用的推導規則,factory_v4
中的Arg
會被推導成X&
。這個時候factory_v4
和std::forwrd
等價於:
shared_ptr<A> factory_v4(X& arg)
{
return shared_ptr<A>(new A(std::forward<X&>(arg)));
}
X& std::forward(X& a)
{
return static_cast<X&>(a);
}
這個時候傳給A
的參數類型是X&
,即調用的是拷貝構造函數A(X&)
。符合預期。
傳入右值
X createX();
auto a = factory_v4<A>(createX());
根據萬能引用推導規則,factory_v4
中的Arg
會被推導成X
。這個時候factory_v4
和std::forwrd
等價於:
shared_ptr<A> factory_v4(X&& arg)
{
return shared_ptr<A>(new A(std::forward<X>(arg)));
}
X&& forward(X& a) noexcept
{
return static_cast<X&&>(a);
}
此時,std::forward
作用與std::move
一樣,隱藏掉了arg
的名字,返回對應的右值引用。這個時候傳給A
的參數類型是X&&
,即調用的是移動構造函數A(X&&)
,符合預期。
總結
這篇文章,我們主要是繼續第四講的內容,一步步學習了完美轉發的概念以及如何使用右值解決參數透傳的問題,實現完美轉發。
[1] https://github.com/CnTransGroup/EffectiveModernCppChinese/blob/master/src/1.DeducingTypes/item1.md
【往期推薦】
【重學C++】02 | 脫離指針陷阱:深入淺出 C++ 智能指針
【重學C++】04 | 說透C++右值引用、移動語義、完美轉發(上)