c++右值引用、移動語義、完美轉發

来源:https://www.cnblogs.com/spacerunnerZ/archive/2023/11/07/17815655.html
-Advertisement-
Play Games

1. 左值、右值、左值引用以及右值引用 左值:一般指的是在記憶體中有對應的存儲單元的值,最常見的就是程式中創建的變數 右值:和左值相反,一般指的是沒有對應存儲單元的值(寄存器中的立即數,中間結果等),例如一個常量,或者表達式計算的臨時變數 int x = 10 int y = 20 int z = x ...


1. 左值、右值、左值引用以及右值引用

  • 左值:一般指的是在記憶體中有對應的存儲單元的值,最常見的就是程式中創建的變數
  • 右值:和左值相反,一般指的是沒有對應存儲單元的值(寄存器中的立即數,中間結果等),例如一個常量,或者表達式計算的臨時變數
int x = 10 
int y = 20
int z = x + y 
//x, y , z 是左值
//10 , 20,x + y 是右值,因為它們在完成賦值操作後即消失,沒有占用任何資源
  • 左值引用:C++中採用 &對變數進行引用,這種常規的引用就是左值引用
  • 右值引用:右值引用最大的作用就是讓一個左值達到類似右值的效果(下麵程式舉例),讓變數之間的轉移更符合“語義上的轉移”,以減少轉移之間多次拷貝的開銷。右值引用符號是&&。

例如,對於以下程式,我們要將字元串放到vector中,且我們後續的代碼中不再用到x:

std::vector<std::string> vec;
std::string x = "abcd";
vec.push_back(x);
std::cout<<"x: "<<x<<"\n";
std::cout<<"vector: "<< vec[0]<<"\n";

//-------------output------------------
// x: abcd
// vector: abcd

 該程式在真正執行的過程中,實際上是複製了一份字元串x,將其放在vector中,這其中多了一個拷貝的開銷和記憶體上的開銷。但如果x以及沒有作用了,我們希望做到的是 真正的轉移,即x指向的字元串移動到vector中,不需要額外的記憶體開銷和拷貝開銷。因此我們希望讓變數 x傳入到push_back 表現的像一個右值 ,這個時候就體現右值引用的作用,只需要將x的右值引用傳入就可以。

改進成如下代碼:

std::vector<std::string> vec;
std::string x = "abcd";
vec.push_back(std::move(x));              <---------------  使用了std::move,任何的左值/右值通過std::move都轉化為右值引用
std::cout<<"x: "<<x<<"\n";
std::cout<<"vector: "<< vec[0]<<"\n";
//-------------output------------------
// x: 
// vector: abcd

可以看到,完成`push_back`後x是空的。

 

 2. 移動語義

 移動語義是通過移動構造移動賦值避免無意義的拷貝操作。

2.1 使用std::move實現移動構造

定義:採用右值引用作為參數的構造函數又稱作移動構造函數。此時不需要額外的拷貝操作,也不需要新分配記憶體。

使用場景:對於一個值(比如數組、字元串、對象等)如果在執行某個操作後不再使用,那麼這個值就叫做將亡值(Expiring Value),因此對於本次操作我們就沒必要對該值進行額外的拷貝操作,而是希望直接轉移,儘可能減少額外的拷貝開銷,操作後該值也不再占用額外的資源。

使用函數:std::move,任何的左值/右值通過std::move都轉化為右值引用

看如下例子,

#include <iostream>
#include <vector>
#include <string>

class A {
  public:
    A(){}
    A(size_t size): size(size), array((int*) malloc(size)) {
        std::cout 
          << "create Array,memory at: "  
          << array << std::endl;
        
    }
    ~A() {
        free(array);
    }
    A(A &&a) : array(a.array), size(a.size) {
        a.array = nullptr;
        std::cout 
          << "Array moved, memory at: " 
          << array 
          << std::endl;
    }
    A(A &a) : size(a.size) {
        array = (int*) malloc(a.size);
        for(int i = 0;i < a.size;i++)
            array[i] = a.array[i];
        std::cout 
          << "Array copied, memory at: " 
          << array << std::endl;
    }
    size_t size;
    int *array;
};
int main() {
    std::vector<A> vec;
    A a = A(10);
    vec.push_back(a);   
    return 0;   
}

//----------------output--------------------
// create Array,memory at: 0x600002a28030 // A a = A(10); 調用了 構造函數A(size_t size){}
// Array copied, memory at: 0x600002a28050 //vec push的時候拷貝一份,調用構造函數A(A &a){}

從輸出可以看到,每次進行push_back的時候,會重新創建一個對象,調用了左值引用A(A &a) : size(a.size)對應的構造函數,將對象中的數組重新深拷貝一份。

如果該用右值引用進行優化,如下

int main () {
    std::vector<A> vec;
    A a = A(10);
    vec.push_back(std::move(a));   
    return 0;   
}

//----------------output--------------------
// create Array,memory at: 0x600003a84030
// Array moved, memory at: 0x600003a84030

可以看到,這個時候雖然也重新創建了一個對象,但是調用的是這個構造函數A(A &&a) : array(a.array), size(a.size)(這種採用右值引用作為參數的構造函數又稱作移動構造函數),此時不需要額外的拷貝操作,也不需要新分配記憶體。

 

3. 完美轉發

使用函數:std::forward,如果傳遞的是左值轉發的就是左值引用,傳遞的是右值轉發的就是右值引用。

 

3.1 引用摺疊

在具體介紹std::forward之前,需要先瞭解C++的引用摺疊規則,對於一個值引用的引用最終都會被摺疊成左值引用或者右值引用。

  • T& & -> T& (對左值引用的左值引用是左值引用)
  • T& && -> T& (對左值引用的右值引用是左值引用)
  • T&& & ->T& (對右值引用的左值引用是左值引用)
  • T&& && ->T&& (對右值引用的右值引用是右值引用)

總結一句話,只有對於右值引用的右值引用摺疊完還是右值引用,其他都會被摺疊成左值引用。

 

3.2 使用std::forward實現完美轉發

std::forward的作用就是完美轉發,確保轉發過程中引用的類型不發生任何改變,左值引用轉發後一定還是左值引用,右值引用轉發後一定還是右值引用!

下麵是一個使用 std::forward 的例子:

#include <iostream>
#include <utility>
 
void func(int& x) {
    std::cout << "lvalue reference: " << x << std::endl;
}
 
void func(int&& x) {
    std::cout << "rvalue reference: " << x << std::endl;
}
 
template<typename T>
void wrapper(T&& arg) {
    func(std::forward<T>(arg));
}
 
int main() {
    int x = 42;
    wrapper(x);  // lvalue reference: 42
    wrapper(1);  // rvalue reference: 1
    return 0;
}

在上面的例子中,我們定義了兩個函數 func,一個接受左值引用,另一個接受右值引用。然後我們定義了一個模板函數 wrapper,在 wrapper 函數中,我們使用 std::forward 函數將參數 arg 轉發給 func 函數。通過使用 std::forward,我們可以確保 func 函數接收到的參數的左右值特性與原始參數保持一致。

  • 當向wrapper裡面傳入x的時候,wrapper推導認為 T是一個左值引用int &,通過引用摺疊原則(看萬能引用文章)int && + & = int &,相當於wrapper(int& arg),同時我們知道了T推導為int&,那麼在向func傳遞的時候,就是func(std::forward<int&> (arg)) ,那麼func會以左值引用的形式 func(int& x) 調用arg。
  • 當向wrapper裡面傳入1的時候,wrapper推導認為T是一個右值引用int&& ,通過引用摺疊原則,int && + && =int&& ,相當於wrapper(int&& arg),同時我們知道了T推導為int&&,那麼在向func傳遞的時候,就是func(std::forward<int&&>(arg)),那麼func會以左值引用的形式func(int&& x)調用arg。

 

另一個例子:

class Test{};

void B(Test& a) {cout << "B&"  << endl;}
void B(Test&& a) {cout << "B&&" << endl;}
template<typename T> void A(T &&a) { B(std::forward<T>(a)); }

int main()
{
  Test a;
  A(std::move(a));
  A(a);
  return 0;    
}


//////
//輸出結果
B&&
B&

 

 

 

參考鏈接:https://zhuanlan.zhihu.com/p/469607144

https://www.jb51.net/article/278300.htm


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

-Advertisement-
Play Games
更多相關文章
  • 本節介紹Util應用框架如何進行驗證. 概述 驗證是業務健壯性的基礎. .Net 提供了一套稱為 DataAnnotations 數據註解的方法,可以對屬性進行一些基本驗證,比如必填項驗證,長度驗證等. Util應用框架使用標準的數據註解作為基礎驗證,並對自定義驗證進行擴展. 基礎用法 引用Nuge ...
  • 本文介紹了結構型設計模式中的橋接模式,講解了它的特點和相關構成,並通過相應的案例,使用Java代碼進行演示。 ...
  • 本節介紹Util應用框架對AspectCore AOP的使用. 概述 有些問題需要在系統中全局處理,比如記錄異常錯誤日誌. 如果在每個出現問題的地方進行處理,不僅費力,還可能產生大量冗餘代碼,並打斷業務邏輯的編寫. 這類跨多個業務模塊的非功能需求,被稱為橫切關註點. 我們需要把橫切關註點集中管理起來 ...
  • 1. HashMap和HashT able的區別 HashMap和Hashtable是兩種常見的哈希表數據結構,它們在實現上有一些區別。 線程安全性:Hashtable是線程安全的,而HashMap不是。Hashtable的方法都是同步的,可以在多線程環境中使用,但這樣會造成一定的性能開銷。Hash ...
  • kubelet 簡介 kubernetes 分為控制面和數據面,kubelet 就是數據面最主要的組件,在每個節點上啟動,主要負責容器的創建、啟停、監控、日誌收集等工作。它是一個在每個集群節點上運行的代理,負責確保節點上的容器根據PodSpec(Pod定義文件)正確運行。 Kubelet執行以下幾項 ...
  • 機器學習是通過研究數據和統計信息使電腦學習的過程。機器學習是邁向人工智慧(AI)的一步。機器學習是一個分析數據並學會預測結果的程式。 數據集 在電腦的思維中,數據集是任何數據的集合。它可以是從數組到完整資料庫的任何東西。 數組的示例: [99,86,87,88,111,86,103,87,94, ...
  • @目錄山茶花100ptsT1區間逆序對60pts100pts 區間操作固定套路,轉化為首碼操作dream20pts 神奇分塊杭州:轉化題意,正難則反正難則反(或者對於這種有刪邊操作的題), 我們看成反向加邊看題:構造坐飛機:斜率優化DP抓頹 : 啟髮式合併 + stl大雜燴討厭的線段樹Foo Fig ...
  • Spring5框架概述 Spring是輕量級的開源的JavaEE框架。 Spring可以解決企業應用開發的複雜性。 Spring有兩個核心部分:IOC和AOP IOC:控制反轉,把創建對象過程交給Spring進行管理 AOP:面向切麵,不修改源代碼進行功能增強 Spring特點 方便解耦,簡化開發( ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...