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;
}