C++17 ———— std::optional、std::variant和std::any


std::optional

The class template std::optional manages an optional contained value, i.e. a value that may or may not be present.
A common use case for optional is the return value of a function that may fail.

這個東西比較難講清楚具體是干啥的,這里直接舉一個例子:比如說有一個讀取文件的函數,這個函數會把文件的內容,存儲在一個string中,作為結果返回。

因為讀取可能會失敗,所以這個函數有必要返回一個信息,來表明讀取文件是否成功,如果用返回空字符串的方式來表示讀取失敗,不太好,因為文件可能本身就是空文件,所以空字符串不能代表讀取失敗。

在C++17之前,可以這么寫這個函數:

string ReadFileAsString(const string& path, bool& isSuccess)
{
	ifstream stream(path);
	string res;
	if(stream)
	{
		...//做讀取操作, 存到res中
		stream.close();
		isSuccess = true;
	}
	else
		isSuccess = false;
		
	return res;
}

而通過std::optional,就可以這么寫:

#include <optional>
// 函數返回類型由string改成了optional<string>
std::optional<string> ReadFileAsString(const string& path)
{
	ifstream stream(path);
	string res;
	if(stream)
	{
		...//做讀取操作, 存到res中
		stream.close();
		return res;//
	}
	else
		return {};//注意返回的是空的
}

int main()
{
	std::optional<string> data = ReadFileAsString("data.txt");//這里的data類似一個智能指針
	if(data)
		cout << "Successfully read file:" << data.value() << endl;
	else
		cout << "Failed to read file" << endl;
		
	return 0;
}

也可以使用optional的value_or函數,當結果失敗時賦予初始值:

string res = data.value_or("Read Data Failed");// 如果data為空,則res初始化為括號里的值

總的來說,std::optional可以表示特定類型的失敗的情況,函數返回類型為std::optional<T>,當正常返回T時,代表返回正常結果,當返回{}時,代表返回錯誤結果。
std::optional<T>顧名思義,可選的,它可以返回T對象,也可以不返回T對象。



std::variant

在C++11里,如果想要一個函數返回多個變量,可以讓其返回std::pair或者std::tuple,C++17提供了std::variant:

The class template std::variant represents a type-safe union. An instance of std::variant at any given time either holds a value of one of its alternative types, or in the case of error - no value (this state is hard to achieve, see valueless_by_exception).

看std::variant,舉個簡單的例子:

std::variant<string, int> data;
data = "hello";
cout << std::get<string>(data) <<endl;//print hello
data = 4;
cout << std::get<int>(data) <<endl;//print 4
cout << std::get<string>(data) <<endl;//編譯通過,但是runtime會報錯,顯示std::bad_variant_access
data = false;//能編譯通過
cout << std::get<bool>(data) <<endl;//這句編譯失敗

再介紹一些相關的api:

//std::variant的index函數
data.index();// 返回一個整數,代表data當前存儲的數據的類型在<>里的序號,比如返回0代表存的是string, 返回1代表存的是int

// std::get的變種函數,get_if
auto p = std::get_if<std::string>(&data);//p是一個指針,如果data此時存的不是string類型的數據,則p為空指針,別忘了傳的是地址

利用C++17的特性,可以寫出這樣的代碼:

// 如果data存的數據是string類型的數據
if(auto p = std::get_if<string>(&data)){
	string& s = *p;
}

union與std::variant的區別
我發現std::variant跟之前學的union很類似,這里回顧一下C++的union關鍵字,詳情可以參考這里

// union應該是C++11推出來的
union test
{
     char mark;
     double num;
     float score;// 共享內存
};

注意,這里sizeof(test),返回的是最長的數據成員對應的大小,32位即是double的八個字節,而std::variant的大小是所有成員的總和,比如:

variant<int, double>s;
cout << sizeof(int) <<endl;//4
cout << sizeof(double) <<endl;//8
cout << sizeof(s) <<endl;//16,這里應該都會有字節對齊
//sizeof(s) = sizeof(int) + sizeof(double);

所以union和variant又有這么個區別:

  • union共享內存,所以存儲效率更高,而variant內部成員各有各的存儲空間,所以更加type-safe,所以說,variant可以作為union的替代品,如下圖所示:
    在這里插入圖片描述

利用std::variant改進之前的讀取函數
前面使用std::optional,創建了一個返回類型為std::optional<string>的函數,在讀取成功時,返回對應的string,否則返回{}(其實是利用initializer_list創建了空的std::optional返回),然后用戶可以通過判斷返回的data是否為空來判斷讀取是否成功,這樣寫用戶只能知道是否讀取失敗,不能知道具體失敗的原因,而用std::variant可以改進這一點,代碼如下:

enum class ErrorCode
{
	None,
	NotFound,
	NoAccess
};

std::variant<string, ErrorCode> ReadFileAsString(const string& path)
{
	ifstream stream(path);
	string res;
	if(stream)
	{
		...//做讀取操作, 存到res中
		stream.close();
		return res;
	}
	else
		return ErrorCode::NotFound;
}

總的來說,std::variant可以返回多種結果,它們的類型都是std::variant<T1, T2...>
std::variant<T1, T2...>顧名思義,多選的,它可以返回T1對象,也可以返回T2、T3等對象,與union很像。



std::any

在C++17之前,可以使用void*來作為存儲任意類型對象的地址的指針,但是void*並不是類型安全的,C++17推出了std::any,可以用於存儲任何數據類型的對象。

std::any的用法如下:

// 方法一,創建一個std::any對象
std::any data = std::make_any<int>(4);

// 方法二,創建一個std::any對象
std::any data;
data = 4;

// 可以對data進行任意類型的賦值
data = "hello world";
data = false;

可以看出來,std::any的用法與std::variant的用法很像,std::variant需要在創建該對象時在<>里指定可以接受的參數類型,而std::any不需要指定參數類型(因為它可以接受任何類型),鑒於二者各自的特點,std::any顯然更方便,但是std::any在type safe上不如std::variant,比如下面這段代碼:

std::any data;
data = "Hello World";
// 下面這個轉換是不對的,因為上面的data類型是const char*, 並不是string
string s = std::any_cast<string>(data);


std::any到底是怎么實現的

由於STL都是在頭文件里實現的,所以可以直接到std::any的頭文件<any>里去看一下源代碼:

any類有一個私有成員變量,是一個union:

union
{
	_Storage_t _Storage;// 看上去是用來存儲對應對象的
	max_align_t _Dummy;// 最大字節對齊的size?
};

_Storage_t是一個結構體:

struct _Storage_t
{
	union
	{
		aligned_union_t<_Any_trivial_space_size, void *> _TrivialData;
		_Small_storage_t _SmallStorage;//用來存儲小的對象
		_Big_storage_t _BigStorage;//用來存儲大的對象
	};
	uintptr_t _TypeData;
};

再看看_Big_storage_t類是怎么定義的:

struct _Big_storage_t
{
	// 用於負責Padding的
	char _Padding[_Any_small_space_size - sizeof(void *)]; // "push down" the active members near to _TypeData
	void * _Ptr;//存儲了一個void*指針
	const _Any_big_RTTI * _RTTI;//用來負責RTTI的
};
// 在別的地方可以看到
// constexpr size_t _Any_small_space_size = (_Small_object_num_ptrs - 2) * sizeof(void *);
// constexpr int _Small_object_num_ptrs = 6 + 16 / sizeof (void *);
// 所以_Any_small_space_size - sizeof(void *) = (4 + 16/sizeof(void *))* sizeof(void *) - sizeof(void*)
// 在32位上sizeof(void*) = 4, 所以等同於:
// char _Padding[32];

而_Small_storage_t的類定義如下:

// 別的地方有:
using aligned_union_t = typename aligned_union<_Len, _Types...>::type;//(since C++14)

struct _Small_storage_t
{
	aligned_union_t<_Any_small_space_size, void *> _Data;// 關於aligned_union會在后面詳細介紹
	const _Any_small_RTTI * _RTTI;
};

也就是說,std::any,存儲對象的方式有兩種,對於比較小的對象,會存儲在擁有多種類型的union里,該對象位於stack上 ,此時用法與std::variant非常類似,對於比較大的對象,會存在堆上,用void*存儲對應堆上的地址,在32位的機器上,這個size的分界值是32個bytes

最后總結一下std::variant與std::any的區別:

  • std::variant支持的類型有限,而std::any可以支持任何類型
  • std::variant更type safe
  • std::variant理論上效率更高,因為對於較大的對象,它不會在heap上去分配內存
  • std::any感覺不如std::variant,當實在得用std::any、不可用std::variant的時候,可能需要思考一下自己的代碼設計是不是有問題


總結三種類型

std::optional、std::variant和std::any的保存對象范圍是逐漸擴大的:

  • std::optional,對於<T>,只有兩種情況,一種是有T,一種是沒T(failture的情況)
  • std::variant,需要<>里指定任意類型,它可以是T1、T2…
  • std::any,則不需要用<>來指定包含的對象類型,因為它支持任意對象,可以直接用任意類型的對象對其賦值


關於std::aligned_union

什么是std::aligned_union
在cplusplus網站上,是這么介紹的:

template <size_t Len, class... Types> struct aligned_union;

Aligned union
Obtains a POD type suitable for use as storage for any object whose type is listed in Types, and a size of at least Len.
The obtained type is aliased as member type aligned_union::type.
Notice that this type is not a union, but the type that could hold the data of such a union.

關於POD
這里有必要介紹一下什么是POD type,POD全稱是Plain Old Data,指的一種類,這種類沒有自定義的ctor、dtor、copy assignment operator和虛函數,也被稱為聚合類(aggregate class),POD里也沒有成員函數

A Plain Old Data Structure in C++ is an aggregate class that contains only PODS as members, has no user-defined destructor, no user-defined copy assignment operator, and no nonstatic members of pointer-to-member type.

aligned_union用法
再回頭看aligned_union,它是一個模板類,一個alighned_union對象代表着一塊內存區間(storage),這塊區間的大小至少為Len個字節,這塊區間可以作為一個union所需要的內存空間。

具體的可以看這么個例子:

#include <memory>
#include <type_traits>

// include和use是兩個簡單的類
struct include
{
    std::string file;
};
struct use
{
    use(const std::string &from, const std::string &to) : from{ from }, to{ to }
    {
    }
    std::string from;
    std::string to;
};


{
	// 分配一塊區間,這個區間可以作為include和use類對象的union
    std::aligned_union<sizeof(use), include, use>::type storage;

	// 前面只是分配了內存,沒有創建真正的union
	// 取storage的內存地址,轉化為void*,然后使用placement new在對應的位置創建一個use類對象
    use * p = new (static_cast<void*>(std::addressof(storage))) use(from, to);

    p->~use();.// 手動調用析構函數
}

再舉個例子,這個是cplusplus官方給的例子:

// aligned_union example
#include <iostream>
#include <type_traits>

// 一個簡單的Union類,它可以存int、char或double類型
union U {
  int i;
  char c;
  double d;
  U(const char* str) : c(str[0]) {}
};       // non-POD

// sizeof(U)會取union里最大的數據成員,即sizeof(double)
typedef std::aligned_union<sizeof(U),int,char,double>::type U_pod;

int main() {
	// U_pod代表U這種union的內存分配器
    U_pod a,b;              // default-initialized (ok: type is POD)
    
    // 在a對應的地址,調用placement new,調用U的ctor
    new (&a) U ("hello");   // call U's constructor in place
    
    // 由於b和a是同種類型,而且是POD,可以直接進行賦值
    b = a;                  // assignment (ok: type is POD)
    
    // 把b直接轉換成U&類型,然后print其i
    std::cout << reinterpret_cast<U&>(b).i << std::endl;

    return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM