【重學C++】05 | 說透右值引用、移動語義、完美轉發(下)

来源:https://www.cnblogs.com/huiwancode/archive/2023/05/29/17440005.html
-Advertisement-
Play Games

## 文章首發 [【重學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對調用者是透明的。要達到這個目的有兩個前提:

  1. 傳給factory_v1的入參arg能夠完完整整(包括引用屬性、const屬性等)得傳給T的構造函數。
  2. 工廠函數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,推導出模版函數fooParamTypeparam的類型。

類型推導的規則有很多,大家感興趣可以去看看《Effective C++》[1],這裡,我們只介紹一種比較特殊的萬能引用。 萬能引用的模版函數格式如下:

template<typename T>
void foo(T&& param);

萬能引用的ParamTypeT&&,既不能是const T&&,也不能是std::vector<T>&&

萬能引用的規則有三條:

  1. 如果expr是左值,Tparam都會被推導成左值引用。
  2. 如果expr是右值,T會被推導成對應的原始類型,param會被推導成右值引用(註意,雖然被推導成右值引用,但由於param有名字,所以本身還是個左值)。
  3. 在推導過程中,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_v4std::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_v4std::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

END

【往期推薦】

【重學C++】01| C++ 如何進行記憶體資源管理?

【重學C++】02 | 脫離指針陷阱:深入淺出 C++ 智能指針

【重學C++】03 | 手擼C++智能指針實戰教程

【重學C++】04 | 說透C++右值引用、移動語義、完美轉發(上)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ## 教程簡介 Angular Highcharts是一個基於Angular的開源組件,可在Angular應用程式中提供優雅且功能豐富的高圖表可視化,並可與Angular組件無縫配合使用。 [Angular Highcharts入門教程](https://www.itbaoku.cn/tutoria ...
  • ## 教程簡介 Flume是Apache下麵的一個分散式組件,它提供高效,可靠的收集,整合,傳輸日誌數據的服務。Flume可以理解成一個管道,它連接數據的生產者和消費者,它從數據的生產者(Source)獲取數據,保存在自己的緩存(Channel)中,然後通過Sink發送到消費者。它不對數據做保存和復 ...
  • ## 測試環境: > MySQL版本:8.0 資料庫表:T (主鍵id,唯一索引c,普通欄位d) ![](http://img.javastack.cn/1685072039483867.png) 如果你的業務設計依賴於自增主鍵的連續性,這個設計假設自增主鍵是連續的。但實際上,這樣的假設是錯的,因為 ...
  • 編碼如下:#include <stdio.h> void swap(int* x,int* y ){ int tmp; tmp=*x; *x=*y; *y=tmp ; }; int main(){ int a=4; int b=5; printf("befer\n"); printf("a=%d\n ...
  • @[TOC] # 1.背景 最近項目是國際項目,所以需要經常需要用到UTC時間和local時間的轉換。 所以整理了一下時間戳工具類,方便使用。 這裡主要用到的包就是datatime、time、pytz。 # 2. 遇到的坑 直接看測試案例 ```python tzinfo=pytz.timezone ...
  • 本文將為大家詳細講解Java中Properties配置類怎麼用,這是我們進行開發時經常用到的知識點,也是大家在學習Java中很重要的一個知識點,更是我們在面試時有可能會問到的問題!文章較長,乾貨滿滿,建議大家收藏慢慢學習。文末有本文重點總結,主頁有全系列文章分享。技術類問題,歡迎大家和我們一起交流討... ...
  • SQLAlchemy是著名的ORM(Object Relational Mapping-對象關係映射)框架。其主要作用是在編程中,把面向對象的概念跟資料庫中表的概念對應起來。對許多語言(例如JAVA/PYTHON)來說就是定義一個對象,並且這個對象對應著一張資料庫的表。而這個對象的實例,就對應著表中... ...
  • 摘要:Python Web程式員必看系列,學習如何壓縮 JS 代碼。 本文分享自華為雲社區《Python壓縮JS文件,PythonWeb程式員必看系列,重點是 slimit》,作者: 夢想橡皮擦 。 本篇博客將學習壓縮 JS 代碼,首先要學習的模塊是 jsmin。 jsmin 庫 Python 中的 ...
一周排行
    -Advertisement-
    Play Games
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • 目錄前言PostgreSql安裝測試額外Nuget安裝Person.cs模擬運行Navicate連postgresql解決方案Garnet為什麼要選擇Garnet而不是RedisRedis不再開源Windows版的Redis是由微軟維護的Windows Redis版本老舊,後續可能不再更新Garne ...
  • C#TMS系統代碼-聯表報表學習 領導被裁了之後很快就有人上任了,幾乎是無縫銜接,很難讓我不想到這早就決定好了。我的職責沒有任何變化。感受下來這個系統封裝程度很高,我只要會調用方法就行。這個系統交付之後不會有太多問題,更多應該是做小需求,有大的開發任務應該也是第二期的事,嗯?怎麼感覺我變成運維了?而 ...
  • 我在隨筆《EAV模型(實體-屬性-值)的設計和低代碼的處理方案(1)》中介紹了一些基本的EAV模型設計知識和基於Winform場景下低代碼(或者說無代碼)的一些實現思路,在本篇隨筆中,我們來分析一下這種針對通用業務,且只需定義就能構建業務模塊存儲和界面的解決方案,其中的數據查詢處理的操作。 ...
  • 對某個遠程伺服器啟用和設置NTP服務(Windows系統) 打開註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer 將 Enabled 的值設置為 1,這將啟用NTP伺服器功 ...
  • title: Django信號與擴展:深入理解與實踐 date: 2024/5/15 22:40:52 updated: 2024/5/15 22:40:52 categories: 後端開發 tags: Django 信號 松耦合 觀察者 擴展 安全 性能 第一部分:Django信號基礎 Djan ...
  • 使用xadmin2遇到的問題&解決 環境配置: 使用的模塊版本: 關聯的包 Django 3.2.15 mysqlclient 2.2.4 xadmin 2.0.1 django-crispy-forms >= 1.6.0 django-import-export >= 0.5.1 django-r ...
  • 今天我打算整點兒不一樣的內容,通過之前學習的TransformerMap和LazyMap鏈,想搞點不一樣的,所以我關註了另外一條鏈DefaultedMap鏈,主要調用鏈為: 調用鏈詳細描述: ObjectInputStream.readObject() DefaultedMap.readObject ...
  • 後端應用級開發者該如何擁抱 AI GC?就是在這樣的一個大的浪潮下,我們的傳統的應用級開發者。我們該如何選擇職業或者是如何去快速轉型,跟上這樣的一個行業的一個浪潮? 0 AI金字塔模型 越往上它的整個難度就是職業機會也好,或者說是整個的這個運作也好,它的難度會越大,然後越往下機會就會越多,所以這是一 ...
  • @Autowired是Spring框架提供的註解,@Resource是Java EE 5規範提供的註解。 @Autowired預設按照類型自動裝配,而@Resource預設按照名稱自動裝配。 @Autowired支持@Qualifier註解來指定裝配哪一個具有相同類型的bean,而@Resourc... ...