【重學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
  • 前言 在我們開發過程中基本上不可或缺的用到一些敏感機密數據,比如SQL伺服器的連接串或者是OAuth2的Secret等,這些敏感數據在代碼中是不太安全的,我們不應該在源代碼中存儲密碼和其他的敏感數據,一種推薦的方式是通過Asp.Net Core的機密管理器。 機密管理器 在 ASP.NET Core ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 順序棧的介面程式 目錄順序棧的介面程式頭文件創建順序棧入棧出棧利用棧將10進位轉16進位數驗證 頭文件 #include <stdio.h> #include <stdbool.h> #include <stdlib.h> 創建順序棧 // 指的是順序棧中的元素的數據類型,用戶可以根據需要進行修改 ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • C總結與剖析:關鍵字篇 -- <<C語言深度解剖>> 目錄C總結與剖析:關鍵字篇 -- <<C語言深度解剖>>程式的本質:二進位文件變數1.變數:記憶體上的某個位置開闢的空間2.變數的初始化3.為什麼要有變數4.局部變數與全局變數5.變數的大小由類型決定6.任何一個變數,記憶體賦值都是從低地址開始往高地 ...
  • 如果讓你來做一個有狀態流式應用的故障恢復,你會如何來做呢? 單機和多機會遇到什麼不同的問題? Flink Checkpoint 是做什麼用的?原理是什麼? ...
  • C++ 多級繼承 多級繼承是一種面向對象編程(OOP)特性,允許一個類從多個基類繼承屬性和方法。它使代碼更易於組織和維護,並促進代碼重用。 多級繼承的語法 在 C++ 中,使用 : 符號來指定繼承關係。多級繼承的語法如下: class DerivedClass : public BaseClass1 ...
  • 前言 什麼是SpringCloud? Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的開發便利性簡化了分散式系統的開發,比如服務註冊、服務發現、網關、路由、鏈路追蹤等。Spring Cloud 並不是重覆造輪子,而是將市面上開發得比較好的模塊集成進去,進行封裝,從 ...
  • class_template 類模板和函數模板的定義和使用類似,我們已經進行了介紹。有時,有兩個或多個類,其功能是相同的,僅僅是數據類型不同。類模板用於實現類所需數據的類型參數化 template<class NameType, class AgeType> class Person { publi ...
  • 目錄system v IPC簡介共用記憶體需要用到的函數介面shmget函數--獲取對象IDshmat函數--獲得映射空間shmctl函數--釋放資源共用記憶體實現思路註意 system v IPC簡介 消息隊列、共用記憶體和信號量統稱為system v IPC(進程間通信機制),V是羅馬數字5,是UNI ...