現代 C++ 性能飛躍之:移動語義

来源:https://www.cnblogs.com/englyf/archive/2023/06/09/17468073.html
-Advertisement-
Play Games

*以下內容為本人的學習筆記,如需要轉載,請聲明原文鏈接[ 微信公眾號「ENG八戒」](https://mp.weixin.qq.com/s/Xd_FwT8E8Yx9Vnb64h6C8w) > 帶給現代 C++ 性能飛躍的特性很多,今天一邊聊技術,一邊送福利! ![](https://img2023. ...


*以下內容為本人的學習筆記,如需要轉載,請聲明原文鏈接 微信公眾號「ENG八戒」https://mp.weixin.qq.com/s/Xd_FwT8E8Yx9Vnb64h6C8w

帶給現代 C++ 性能飛躍的特性很多,今天一邊聊技術,一邊送福利!


過去寫 C/C++ 代碼,大家對數據做傳遞時,都習慣先拷貝再賦值。比如,把數據從 t1 複製到 t2,複製完成後 t2 和 t1 的狀態是一致的,t1 狀態沒變。這裡的狀態指的是對象內部的非靜態成員數據集合。

在程式運行過程中,複製過程既要分配空間又要拷貝內容,對於空間和時間都是種損耗。複製操作,無疑是一門很大的開銷,何況經常觸發資源複製的時候。

來看看普通的函數返回值到底有哪些開銷,

std::string getString()
{
    std::string s;
    // ...

    return s;
}

int main()
{
    std::string str = getString();
    // ...
}

假設你的編譯器還不支持 C++ 11,那麼,在 main() 函數里調用 getString() 時,需要在調用棧里分配臨時對象用於複製 getString() 的返回值 s,複製完成調用 s 的析構函數釋放對象。然後,再調用 std::string 類的複製賦值運算符函數將臨時對象複製到 str,同時調用臨時對象的析構函數執行釋放。

那麼,有沒有技巧可以實現上面示例代碼同樣的效果,同時避免複製?

有的,就是接下來重點介紹的移動(和中國移動無關)。

相對於複製,移動無須重新分配空間和拷貝內容,只需把源對象的數據重新分配給目標對象即可。移動後目標對象狀態與移動前的源對象狀態一致,但是移動後源對象狀態被清空。

實際上,大部份的情況下,數據僅僅需要移動即可,拷貝複製顯得多餘。就像,你從圖書館借書,把自己手機的 SIM 卡拔出來再插到其它手機上,去商店買東西你的錢從口袋移動到收銀櫃等等。

那麼,是不是可以對所有的數據都執行移動?

答案是否定的。在現代 C++ 中,只有右值可以被移動。

左右值概念

在 C++ 11 之前,左右值的劃分比較簡單,只有左值和右值兩種。

但是從 C++ 11 開始,重新把值類別劃分成了五種,左值(lvalue, left value),將亡值(xvalue, expiring value),純右值(prvalue, pure right value),泛左值(glvalue, generalized left value),右值(rvalue, right value)。不過後邊的兩種 glvalue 和 rvalue 是基於前面的三種組合而成。從集合概念來看,glvalue 包含 lvalue 和 xvalue,rvalue 包含 xvalue 和 prvalue。

左右值劃分的依據是:具名和可被移動。

具名,簡單點理解就是定址。可被移動,允許對量的內部資源移動到其它位置,並且保持量自身是有效的,但是狀態不確定。

  • lvalue:具名且不可移動
  • xvalue:具名且可移動
  • prvalue:不具名且可移動

那麼,可以看到泛左值(glvalue)其實就是具名的量,右值就是可移動的量。

以往在往函數傳參的時候,經常有用到值引用的模式,形式如下:

function(T& obj)

T 是類型,obj 是參數。

到了現代 C++,原來的值引用就變成了左值引用,另外還出現了右值引用,形式如下:

function(T&& obj)

那麼 C++ 11 是怎樣實現移動操作的呢?

實現移動操作

移動操作依賴於類內部特殊成員函數的執行,但前提是該對象是可移動的。如果恰好對象是左值(lvalue)呢?

C++ 11 的標準庫就提供了 std::move() 實現左右值轉換操作。std::move() 用於將表達式從 lvalue(左值) 轉換成 xvalue(將亡值),但不會對數值執行移動。當然,使用強制類型轉換也是可以達到同樣目的。

std::move(obj); // 等價於 static_cast<T&&>(obj);

在 stack overflow 上看到對 std::move() 的一段描述,與其說它是一個函數,不如說,它是編譯器對錶達式值評估的方式轉換器。

以往慣常使用 C++ 類定義時,我們都知道有這麼幾個特殊的成員函數:

  • 預設構造函數(default constructor)
  • 複製構造函數(copy constructor)
  • 複製賦值運算符函數(copy assignment operator)
  • 析構函數(destructor)

來看看一個簡單的例子:

class MB // MemoryBlock
{
public:
    // 為下麵代碼演示簡單起見
    // 在 public 定義成員屬性
    size_t size;
    char *buf;

    // 預設構造函數
    explicit MB(int sz = 1024)
        : size(sz), buf(new char[sz]) {}
    // 析構函數
    ~MB() {
        if (buf != nullptr) {
            delete[] buf;
        }
    }
    // 複製構造函數
    MB(const MB& obj)
        : size(obj.size),
          buf(new char[obj.size]) {
        memcpy(buf, obj.buf, size);
    }
    // 複製賦值運算符函數
    MB& operator=(const MB& obj) {
        if (this != &obj) {
            if (buf != nullptr) {
                delete[] buf;
            }
            size = obj.size;
            buf = new char[size]; 
            memcpy(buf, obj.buf, size);
        }
        return *this;
    }
}

為了支持移動操作,從 C++ 11 開始,類定義里新增了兩個特殊成員函數:

  • 移動構造函數(move constructor)
  • 移動賦值運算符函數(move assignment operator)

移動構造函數

在構造新對象時,如果傳入的參數是右值引用對象,就會調用移動構造函數創建對象。如果沒有自定義移動構造函數,那麼編譯器就會自動生成,預設實現是遍歷調用成員屬性的移動構造函數,並移動右值對象的成員屬性數據到新對象。

定義一般聲明形式如下:

T::T(C&& other);

基於上面的簡單例子:

class MB // MemoryBlock
{
public:
    // ...

    // 移動構造函數
    MB(MB&& obj)
        : size(0), buf(nullptr) {
        // 移動源對象數據到新對象
        size = obj.size;
        buf = obj.buf;
        // 清空源對象狀態
        // 避免析構函數多次釋放資源
        obj.size = 0;
        obj.buf = nullptr;
    }
}

可見,移動構造函數的執行過程,僅僅是簡單賦值的過程,不涉及拷貝資源的耗時操作,自然執行效率大大提高。

移動賦值運算符函數

在調用賦值運算符時,如果右邊傳入的參數是右值引用對象,就會調用移動賦值運算符函數。同樣,如果沒有自定義移動賦值運算符函數,那麼編譯器也會自動生成,預設實現是遍歷調用成員屬性的移動賦值運算符函數並移動成員屬性的數據到左邊參數對象。

一般聲明形式如下:

T& T::operator=(C&& other);

基於上面的簡單例子:

class MB // MemoryBlock
{
public:
    // ...

    // 移動賦值運算符函數
    MB& MB::operator=(MB&& obj) {
        if (this != &obj) {
            if (buf != nullptr) {
                delete[] buf;
            }
            // 移動源對象數據到新對象
            size = obj.size;
            buf = obj.buf;
            // 清空源對象狀態
            // 避免析構函數多次釋放資源
            obj.size = 0;
            obj.buf = nullptr;
        }
        return *this;
    }
}

移動賦值運算符函數的執行過程,同樣僅僅是簡單賦值的過程,執行效率明顯遠超複製操作。

總結

回顧文首的示例代碼,由於 C++ 11 加入了返回值優化 RVO(Return Value Optimization) 的特性,所以代碼無需變更即可獲得效率提升。對於部分編譯器而言,比如 IBM Compiler、Visual C++ 2010 等,已經提前具備返回值優化的支持。

對於 RVO 的內容,暫不展開討論,有興趣的同學可以關註公眾號【ENG八戒】瞭解後續更新,關註後甚至可以參與贈書活動!



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

-Advertisement-
Play Games
更多相關文章
  • # 解決JavaScript單線程問題——webWorkers > 參考文檔 [使用 Web Workers - Web API 介面參考 | MDN (mozilla.org)](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Worker ...
  • 基礎知識 知識點梳理見圖: 自己動手實踐案例 案例1: 訪問本地文件 <!DOCTYPE html> <html> <body> <div id="demo"> <h1>XMLHttpRequest 對象</h1> <button type="button" onclick="loadDoc()"> ...
  • vue3 偵聽器 在Vue3中,偵聽器的使用方式與Vue2相同,可以使用watch選項或$watch方法來創建偵聽器。不同之處在於,Vue3中取消了immediate選項,同時提供了新的選項和API。 創建偵聽器 可以使用watch選項或$watch方法來創建偵聽器,語法與Vue2相同。示例如下: ...
  • 報錯信息如下: 代碼如下: <!DOCTYPE html> <html> <body> <div id="demo"> <h1>XMLHttpRequest 對象</h1> <button type="button" onclick="loadDoc()">更改內容</button> </div> ...
  • 一. 目標 個人賬號的設置記憶功能-避免用戶每次登錄之後重新對錶單欄位做展示設置 二、存儲方案 輕量方案 結合localstorage低容量存儲(5M),根據LRU只存最近訪問的20至30張表格列配置數據 全量方案 大記憶體G級別,使用indexedDb進行存儲,有多少表格操作列數據就存多少, 結合第 ...
  • ###為什麼建議使用對象來替換枚舉? ### 在設計模型時,我們經常會使用枚舉來定義類型,比如說,一個員工類 Employee,他有職級,比如P6/P7。順著這個思路,設計一個 Level 類型的枚舉: ``` class Employee { private String name; /** * ...
  • 摘要:本文將探索內核中解析PE文件的相關內容。 本文分享自華為雲社區《驅動開發:內核PE結構VA與FOA轉換》,作者: LyShark 。 本章將探索內核中解析PE文件的相關內容,PE文件中FOA與VA、RVA之間的轉換也是很重要的,所謂的FOA是文件中的地址,VA則是記憶體裝入後的虛擬地址,RVA是 ...
  • WFP框架是微軟推出來替代TDIHOOK傳輸層驅動介面網路通信的方案,其預設被設計為分層結構,該框架分別提供了用戶態與內核態相同的AIP函數,在兩種模式下均可以開發防火牆產品,以下代碼我實現了一個簡單的驅動過濾防火牆。WFP 框架分為兩大層次模塊,用戶態基礎過濾引擎`BFE (BaseFilteri... ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...