深入解析 C++17 中的std::variant與std::visit:從原理到實踐

引言

什么是std::variant

在 C++17 之前,如果你想在一個變量中存儲多種可能的類型,通常會使用?union?或?void*?指針。然而,這些方法都有明顯的缺點。

  • 使用?union?時,類型信息會丟失,使得代碼容易出錯。
  • ?void*?指針則需要手動進行類型轉換和內存管理,容易導致內存泄漏或未定義的行為。

std::variant(變體)作為一種更安全、更方便的多類型容器,應運而生。你可以把它看作是一個可以存儲多種類型中的任一種的類型安全的容器

如下所示

#include <variant>
#include <iostream>int main() {std::variant<int, double, std::string> v1 = 42;std::variant<int, double, std::string> v2 = 3.14;std::variant<int, double, std::string> v3 = "hello";// 訪問存儲的值(不安全,需確保類型正確)std::cout << std::get<int>(v1) << std::endl;// 安全地訪問存儲的值if (auto pval = std::get_if<int>(&v1)) {std::cout << *pval << std::endl;}return 0;
}

與?union?和?void*?的比較

unionvoid*std::variant
類型安全???
自動內存管理???
運行時類型信息???
性能??????
代碼可讀性???

std::variant?的局限性

盡管?std::variant?非常強大,但它并不是萬能的。它的一個主要限制是,雖然它可以存儲多種類型,但在任何給定時間點,它只能存儲其中一種

類型檢查

當你拿到一個?std::variant?對象時,如何知道它當前存儲了哪種類型的值?

在 C++ 這樣的靜態類型(Static Typing)語言中,類型信息在編譯時就已經確定。然而,當你使用?std::variant(變體)時,你實際上是在模擬動態類型(Dynamic Typing)的行為。這意味著你需要在運行時去判斷它究竟存儲了哪種類型的對象。

手動類型檢查

C++ 提供了?std::holds_alternative?和?std::get?等函數,用于檢查和提取?std::variant?中存儲的類型,或者更糟糕的是,使用?std::get_if。這種做法雖然有效,但是很容易出錯。

std::variant<int, double, std::string> v = 42;
if (std::holds_alternative<int>(v)) {int value = std::get<int>(v);  // 安全
} else if (std::holds_alternative<double>(v)) {double value = std::get<double>(v);  // 運行時錯誤!
}

如果你不小心用了錯誤的類型去訪問?std::variant,會拋出一個?std::bad_variant_access?異常。這種情況下,你不得不依賴運行時錯誤檢查,這無疑增加了代碼的復雜性。

方法優點缺點
std::holds_alternative簡單、直觀不能提取值
std::get可以直接提取值類型錯誤會拋出異常
std::get_if可以檢查和提取值,不會拋出異常返回指針,需要額外的空指針檢查

什么是std::visit

當你使用?std::variant?時,一個自然而然的問題是如何處理存儲在其中的不同類型的值。手動檢查和處理多種可能的類型通常很繁瑣,而且容易出錯。這就是?std::visit?發揮作用的地方。

std::visit?提供了一種機制,讓你能夠方便、優雅地處理?std::variant?中存儲的多種可能的類型。它基于訪問者模式(Visitor Pattern),是一種運行時多態的實現。

基本接口

std::visit?的基本接口如下:

template<class Visitor, class... Variants>
constexpr visit(Visitor&& vis, Variants&&... vars);
  • Visitor:一個可調用對象,它應該能夠接受?Variants?中每種類型的值。它通常是一個重載了?operator()?的結構或類。
  • Variants:一個或多個?std::variant?類型的對象。

std::visit?的工作原理

std::visit?的底層原理涉及幾個關鍵概念,包括類型擦除、類型恢復和函數重載解析。這是一個相對復雜的機制,尤其是在涉及模板和變參模板時。以下是?std::visit?的底層工作原理的概述:

  1. 類型擦除std::variant?是一個類型擦除容器,它可以存儲一定范圍內的不同類型的對象。它內部通常有一個聯合體來存儲數據和一個標記來表示當前存儲的類型。
  2. 訪問存儲的值:當?std::visit?被調用時,它首先需要確定?std::variant?當前存儲的具體類型。這是通過檢查內部的類型標記完成的。
  3. 函數模板實例化std::visit?接受一個可調用對象和一個或多個?std::variant?對象。這個可調用對象通常是一個重載的函數對象或 lambda 表達式,其具有多個重載以處理不同的類型。編譯器會為這些重載生成函數模板實例。
  4. 類型恢復和函數調用:一旦確定了?std::variant?中的類型,std::visit?通過生成的模板代碼來“恢復”此類型,并調用與該類型匹配的函數重載。如果有多個?std::variant?參數,std::visit?將處理所有組合的可能性,并調用適當的重載。
  5. 編譯時多態:這一切都在編譯時發生。編譯器生成適用于所有可能的類型組合的代碼。因此,std::visit?實現了一種編譯時的多態,而不是運行時多態(如虛函數)。
  6. 效率和優化:由于大部分工作在編譯時完成,std::visit?通常比運行時類型檢查(如動態類型轉換)更高效。編譯器可以優化函數調用,尤其是在可預測的分支和內聯函數的情況下。

綜上所述,std::visit?的核心在于它能夠在編譯時處理多態性,允許編譯器生成處理?std::variant?中所有可能類型的代碼。這種方法確保了類型安全,并允許進行高效的代碼優化。

簡單使用

讓我們先來看一個簡單的例子,這將幫助你更好地理解?std::variant?和?std::visit?的基本用法。

#include <iostream>
#include <variant>
#include <string>int main() {std::variant<int, double, std::string> myVariant = "Hello, world!";std::visit([](auto&& arg) {std::cout << "The value is: " << arg << std::endl;}, myVariant);return 0;
}

在這個例子中,myVariant?可以存儲?intdouble?或?std::string?類型的值。我們使用?std::visit?來訪問存儲在?myVariant?中的值,并輸出它。

這里,std::visit?接受了一個 lambda 表達式作為參數,這個 lambda 表達式可以接受任何類型的參數(由?auto&&?指定),然后輸出這個參數。

如何優雅地使用?std::visit

使用泛型 lambda?表達式

std::visit?允許你傳入一個可調用對象(callable object),通常是一個 lambda 表達式。現代 C++ 提供了一種特殊的 lambda 表達式,稱為泛型 lambda 表達式(generic lambda)。

泛型 lambda 是一個使用?auto?關鍵字作為參數類型的 lambda 表達式。這意味著 lambda 可以接受任何類型的參數,并在函數體內進行處理。

auto generic_lambda = [](auto x) {// do something with x
};

這種靈活性在處理?std::variant?時尤為有用,因為你可能需要根據多種可能的類型來編寫邏輯。

使用?if constexpr?和類型萃取

if constexpr?是 C++17 引入的一種編譯時?if?語句,它允許在編譯時進行條件判斷。這意味著編譯器會根據條件來優化生成的代碼,這通常會帶來更高的性能。

類型萃取:認識你的類型

類型萃取(Type Traits)是 C++11 引入的一組模板,用于在編譯時獲取類型的屬性。例如,std::is_same_v<T1, T2>?可以告訴你?T1?和?T2?是否是同一種類型。

通過結合?if constexpr?和類型萃取,你可以寫出高度靈活且類型安全的代碼。這也是?std::visit?能發揮最大威力的地方。

?綜合應用:泛型 lambda 與類型判斷

std::variant<int, double, std::string> v = "hello";std::visit([](auto&& arg) {using T = std::decay_t<decltype(arg)>;if constexpr (std::is_same_v<T, int>) {std::cout << "int: " << arg << std::endl;} else if constexpr (std::is_same_v<T, double>) {std::cout << "double: " << arg << std::endl;} else {static_assert(std::is_same_v<T, std::string>);std::cout << "string: " << arg << std::endl;}
}, v);

這里,我們使用了泛型 lambda 來接受任何類型的?arg,然后用?if constexpr?和類型萃取來確定?arg?的實際類型,并據此執行相應的操作。

std::visit和訪問者 模式

一個簡單的?std::visit?使用示例。在這個例子中,我將使用?std::variant?來存儲不同類型的數據,并展示如何使用?std::visit?以類型安全的方式訪問和處理這些數據。

假設我們有一個?std::variant,它可以存儲一個?int、一個?double?或一個?std::string?類型的值。我們將編寫一個訪問者函數對象,這個對象會根據?std::variant?當前存儲的類型執行不同的操作。

#include <iostream>
#include <variant>
#include <string>
#include <functional>// 定義 variant 類型
using MyVariant = std::variant<int, double, std::string>;// 訪問者函數對象
struct VariantVisitor {void operator()(int i) const {std::cout << "處理 int: " << i << std::endl;}void operator()(double d) const {std::cout << "處理 double: " << d << std::endl;}void operator()(const std::string& s) const {std::cout << "處理 string: " << s << std::endl;}
};int main() {MyVariant v1 = 10;        // v1 存儲 intMyVariant v2 = 3.14;      // v2 存儲 doubleMyVariant v3 = "hello";   // v3 存儲 stringstd::visit(VariantVisitor(), v1); // 輸出: 處理 int: 10std::visit(VariantVisitor(), v2); // 輸出: 處理 double: 3.14std::visit(VariantVisitor(), v3); // 輸出: 處理 string: helloreturn 0;
}

在這個例子中:

  • 我們定義了一個?std::variant?類型?MyVariant,它可以存儲?intdouble?或?std::string
  • VariantVisitor?是一個重載了?operator()?的結構體,對每種可能的類型提供了一個處理方法。
  • 在?main?函數中,我們創建了三個?MyVariant?實例,分別存儲不同的類型。
  • 使用?std::visit?調用?VariantVisitor?實例,它會自動選擇并調用與?variant?當前存儲的類型相匹配的重載函數。

這個例子展示了?std::visit?如何提供一種類型安全、靈活的方式來處理存儲在?std::variant?中的不同類型的數據。

使用?std::visit?的優缺點

優點

代碼簡潔

使用?std::visit?可以讓你的代碼變得更加簡潔和組織良好。這正是Bruce Eckel在《Thinking in C++》中所強調的,即“代碼的可讀性和維護性應當是編程中的首要任務”。

考慮一個沒有使用?std::visit?的例子,你可能會這樣寫:

if (std::holds_alternative<int>(v)) {// 處理 int 類型
} else if (std::holds_alternative<double>(v)) {// 處理 double 類型
} else if (std::holds_alternative<std::string>(v)) {// 處理 std::string 類型
}

而使用?std::visit,這些?if-else?語句可以被優雅地替換為一個泛型 lambda 表達式:

std::visit([](auto&& arg) {// 統一處理邏輯
}, v);

這種簡潔性對于代碼的組織和可讀性有著明顯的優勢。簡單來說,簡潔的代碼更容易被理解和維護。

?類型安全

std::visit?還具有類型安全(Type Safety)的優點。這意味著編譯器將在編譯階段檢查類型錯誤,減少了運行時錯誤的風險。這與 C++ 的核心原則一致,即“讓錯誤盡早地暴露出來”。

擴展性

std::visit?的另一個優點是擴展性(Extensibility)。如果?std::variant?添加了新的類型,你只需要更新?std::visit?的訪問器函數,而無需改動其他代碼。

缺點

性能影響

盡管?std::visit?提供了許多優勢,但它并非沒有代價。其中之一就是潛在的性能影響。由于?std::visit?需要進行運行時類型檢查,這可能會引入一定的開銷。

然而,現代編譯器通常會進行優化,使這種開銷最小化。實際上,許多情況下,使用?std::visit?造成的性能損失是可以接受的。

模板代碼膨脹

std::visit?是模板函數,這意味著每一種類型組合都可能生成新的實例代碼,導致所謂的“模板代碼膨脹”(Template Bloat)。

方法代碼簡潔性類型安全性擴展性性能影響代碼膨脹
手動類型檢查 (if-else)
std::visit可變

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/77276.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/77276.shtml
英文地址,請注明出處:http://en.pswp.cn/web/77276.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Dijkstra算法對比圖神經網絡(GNN)

什么是AI模型? AI模型(人工智能模型)是一類模仿人類智能行為的數學模型或算法。它們通過從大量數據中學習,識別模式、做出預測或決策。常見的AI模型包括機器學習模型(如決策樹、神經網絡、支持向量機)和深度學習模型(如卷積神經網絡CNN、循環神經網絡RNN)。簡單來說,…

Yarn 安裝與使用教程

Yarn 安裝與使用教程 Yarn 是一個由 Facebook 開發的 JavaScript 包管理工具&#xff0c;它比傳統的 npm 更加高效、可靠&#xff0c;并且在性能上有所提升。Yarn 主要解決了 npm 安裝速度慢、并發性差、緩存機制不完善等問題&#xff0c;它提供了更快的安裝速度、更穩定的依賴…

Spring Boot 的配置加載順序

Spring Boot 的配置加載順序是“后來居上”——優先級高的配置源會覆蓋優先級低的配置源中的同名配置 覆蓋規則如下&#xff1a; 后加載的配置具有更高的優先級&#xff0c;會覆蓋先加載的配置。如果多個配置源中存在同名配置項&#xff0c;最終生效的是具有最高優先級的那個…

Git分支重命名與推送參數解析

這兩個參數的解釋如下&#xff1a; git branch -M master 中的 -M 參數 -M 是 --move --force 的組合簡寫&#xff0c;表示強制重命名當前分支為 master。如果當前分支已經存在名為 master 的分支&#xff0c;-M 會強制覆蓋它&#xff08;慎用&#xff0c;可能導致數據丟失&…

qt源碼編譯

問題1&#xff1a; 源碼頭文件問題&#xff1a; 有部分頭文件缺少#include<limits>頭文件 home/jetson/qt-everywhere-src-5.15.2/qtbase/include/QtCore/qfloat16.h /home/jetson/qt-everywhere-src-5.15.2/qtbase/src/corelib/text/qbytearraymatcher.h 問題2&…

芯嶺技術XL32F003單片機 32位Cortex M0+ MCU簡單介紹 性能優異

XL32F003單片機是深圳市芯嶺技術有限公司的一款基于 32 位 ARM Cortex-M0 內核的高性能微控制器&#xff0c;提供SOP8/SOP14/SOP16/TSSOP20/SSOP24/QFN20/QFN32多種封裝可選&#xff0c;可滿足不同設計需求。XL32F003可用于工業控制、手持設備、PC 外設、傳感器節點等應用場景&…

計算機圖形學實踐:結合Qt和OpenGL實現繪制彩色三角形

在Qt項目中結合OpenGL與CMake需要配置正確的依賴關系、鏈接庫以及代碼結構設計。以下是具體實現步驟和關鍵要點&#xff1a; 一、環境準備 安裝Qt 確保安裝包含OpenGL模塊的Qt版本&#xff08;如Qt OpenGL、Qt OpenGLWidgets組件&#xff09;。安裝CMake 使用3.10及以上版本&a…

3:QT聯合HALCON編程—海康相機SDK二次程序開發

思路&#xff1a; 1.定義帶UI界面的主函數類 1.1在主函數中包含其它所有類頭文件&#xff0c;進行聲明和實例化&#xff1b;使用相機時&#xff0c;是用公共相機的接口在某一個具體函數中去實例化具體的海康相機對象。 1.2設計界面&#xff1a;連接相機&#xff0c;單次采集&a…

基于大模型底座重構司法信息系統

前置篇章&#xff1a;法律智能體所需的基礎知識 構建一個高效的法律智能體&#xff0c;特別是在基于RAG&#xff08;Retrieval-Augmented Generation&#xff09;架構的背景下&#xff0c;需要融合多種學科和領域的知識。以下是對法律智能體開發和應用所需核心基礎知識的簡要介…

類《雙人成行》3D動作益智冒險類雙人控制游戲開發

服務器端采用了基于開源Kbengine&#xff08;引擎使用C和Python編寫&#xff09;的多人在線游戲服務器&#xff0c;客戶端采用Unity3D。游戲支持線上的雙人聯機房間功能。 資源地址&#xff1a;類《雙人成行》3D動作益智冒險類雙人控制游戲開發教程 | Unity 中文課堂 一、游戲…

Spark--基本介紹

Spark是基于內存的快速&#xff0c;通農用&#xff0c;可拓展的大數據分析計算引擎&#xff0c;Hadoop是一個分布式系統基礎架構 Spark和Hadoop之間的對比和聯系 架構與組件&#xff1a; Hadoop&#xff1a; ■ HDFS&#xff1a;分布式文件系統&#xff0c;負責海量數據存儲。…

05-GPIO原理

一、概述 1、GPIO,即通用I/O(輸入/輸出)端口&#xff0c;是STM32可控制的引腳。STM32芯片的GPIO引腳與外部設備連接起來&#xff0c;可實現與外部通訊、控制外部硬件或者采集外部硬件數據的功能。 2、GPIO的復用:引腳復用是指將單個引腳配置為多個功能的能力。在 STM32 中&…

基于LangChain4J的AI Services實踐:用聲明式接口重構LLM應用開發

基于LangChain4J的AI Services實踐&#xff1a;用聲明式接口重構LLM應用開發 前言&#xff1a;當Java開發遇上LLM編程困境 在LLM應用開發領域&#xff0c;Java開發者常面臨兩大痛點&#xff1a;一是需要手動編排Prompt工程、記憶管理和結果解析等底層組件&#xff0c;二是復雜…

深入解析 Docker 容器進程的 cgroup 和命名空間信息

深入解析 Docker 容器進程的 cgroup 和命名空間信息 在現代 Linux 系統中&#xff0c;控制組&#xff08;cgroup&#xff09;和命名空間&#xff08;namespace&#xff09;是實現容器化技術的核心機制。cgroup 用于管理和限制進程的資源使用&#xff08;如 CPU、內存、I/O&…

【汽車ECU電控數據管理篇】S19文件格式解析篇章

一、S19格式是啥 在電控文件管理的初期階段&#xff0c;我首次接觸到的是 A2L 和 HEX 文件。其中&#xff0c;A2L 文件主要承擔著描述性功能&#xff0c;它詳細地描述了各種參數和配置等相關信息。而 HEX 文件則是一種刷寫文件&#xff0c;其內部明確記錄了具體的地址以及對應的…

python編程相關的單詞

the: 在編程中&#xff0c;“the” 是一個常見的英語單詞&#xff0c;用于指定特定的對象或變量。例如&#xff0c;“the function” 指的是某個特定的函數。 the的拼寫是t,h,e.再讀一次t,h,e and: 在編程中&#xff0c;“and” 是一個邏輯運算符&#xff0c;用于連接兩個條件&…

網絡原理 - 4(TCP - 1)

目錄 TCP 協議 TCP 協議段格式 可靠傳輸 幾個 TCP 協議中的機制 1. 確認應答 2. 超時重傳 完&#xff01; TCP 協議 TCP 全稱為 “傳輸控制協議”&#xff08;Transmission Control Protocol&#xff09;&#xff0c;要對數據的傳輸進行一個詳細的控制。 TCP 協議段格…

python博客爬蟲列表

我希望對指定網頁的&#xff0c;博客列表&#xff0c;獲取url&#xff0c;然后保存成本地文件&#xff0c;用python實現 step1: import requests from bs4 import BeautifulSoup import jsondef get_blog_links(url):headers {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win6…

軟件測試入門學習筆記

今天學習新知識&#xff0c;軟件測試。 什么是軟件測試&#xff1f; 使用人工和自動手段來運行或測試某個系統的過程&#xff0c;目的在于檢驗它是否滿足規定的需求或弄清實際結果與預期結果之間的差別。 軟件測試的目的&#xff1f; 1&#xff09;為了發現程序&#xff0…

uniapp開發2--uniapp中的條件編譯總結

以下是對 uni-app 中條件編譯的總結&#xff1a; 概念&#xff1a; 條件編譯是一種技術&#xff0c;允許你根據不同的平臺或環境&#xff0c;編譯不同的代碼。 在 uni-app 中&#xff0c;這意味著你可以編寫一套代碼&#xff0c;然后根據要編譯到的平臺&#xff08;例如微信小…