【重學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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...