[譯]C++17,optional, any, 和 variant 的更多細節
看到一個介紹 C++17 的系列博文(原文),有十來篇的樣子,覺得挺好,看看有時間能不能都簡單翻譯一下,這是第六篇~
std::optional, std::any, 和 std::variant 有一個共同特點:他們都支援就地構造.另外的,std::variant 還支援訪問者模式.
首先,我們要了解一下這3種資料型別的功能作用.
- std::optional 是一種可能包含也可能不包含某一型別物件的型別.
- std::variant 是一種型別安全的聯合體
- std::any 是一種可以包含任意型別(指可複製型別)物件的型別
我在之前的
Construct in-place
什麼是就地構造呢?以 std::optional<std::string> 為例來說明就是: 所謂就地構造,就是你可以直接使用 std::string 的構造引數來構造 std::optional<std::string>.
下面是一個簡短的示例.
#include <optional> #include <iostream> #include <string> int main() { std::cout << std::endl; // C string literal std::optional<std::string> opt1(std::in_place, "C++17"); // 1 // 5 characters 'C' std::optional<std::string> opt2(std::in_place, 5, 'C'); // 2 // initializer list std::optional<std::string> opt3(std::in_place, { 'C', '+', '+', '1', '7' }); // 3 // Copy constructor std::optional<std::string> opt4(opt3); // 4 std::cout << *opt1 << std::endl; std::cout << *opt2 << std::endl; std::cout << *opt3 << std::endl; std::cout << *opt4 << std::endl; std::cout << std::endl; return 0; }
程式碼中的 opt1(第10行), op2(第13行) 和 op3(第16行) 都使用了 std::in_place 標記來進行構造,這意味著 std::optional 的構造引數將直接用於呼叫 std::string 的建構函式.所以在上述程式碼中, opt1 中 std::string 的建構函式引數即為 C 風格字串(“C++17”), op2 中是5個單字元’C’, op3 中則是初始化列表({ ‘C’, ‘+’, ‘+’, ‘1’, ‘7’ }).另外,程式碼中的 opt4(第19行)並未使用就地構造方法,而是呼叫了 std::optional 的複製建構函式(複製了op3).
程式的輸出如下:
上述的就地構造是不是覺得有些熟悉?其實早在 C++11 中,標準庫容器就引入很多用於增加容器元素的介面方法,這些方法都以 emplace 開頭,功能上就是提供了就地構造的方法.以 std::vector<int> vec 為例,藉助其支援的 emplace_back 方法,我們可以直接呼叫 vec.emplace_back(5) 來增加 vec 的末尾元素,這等同於下面程式碼: vec.push_back(int(5)).
std::variant 還支援 std::visit 方法(即精典的設計模式:訪問者).
Visit a list of variants
std::visit 方法允許你對一個 std::variants 列表應用訪問者模式,而相應的訪問者必須是一個callable型別,所謂 callable 型別,是一種可以被呼叫的型別,通常是一個函式,一個函式物件或者一個 lambda 函式.簡單起見,這裡我僅使用 lambda 函式來舉例說明.
#include <iostream>
#include <vector>
#include <typeinfo>
#include <type_traits>
#include <variant>
int main()
{
std::cout << std::endl;
std::vector<std::variant<char, long, float, int, double, long long>> // 1
vecVariant = { 5, '2', 5.4, 100ll, 2011l, 3.5f, 2017 };
// display each value
for (auto& v : vecVariant) {
std::visit([](auto&& arg) {std::cout << arg << " "; }, v); // 2
}
std::cout << std::endl;
// display each type
for (auto& v : vecVariant) {
std::visit([](auto&& arg) {std::cout << typeid(arg).name() << " "; }, v); // 3
}
std::cout << std::endl;
// get the sum
std::common_type<char, long, float, int, double, long long>::type res{}; // 4
std::cout << "typeid(res).name(): " << typeid(res).name() << std::endl;
for (auto& v : vecVariant) {
std::visit([&res](auto&& arg) {res += arg; }, v); // 5
}
std::cout << "res: " << res << std::endl;
// double each value
for (auto& v : vecVariant) {
std::visit([](auto&& arg) {arg *= 2; }, v); // 6
std::visit([](auto&& arg) {std::cout << arg << " "; }, v);
}
std::cout << std::endl;
return 0;
}
程式碼中我建立了 std::variants 的列表(程式碼第11行).每個 variant 都可以包含以下的任一型別:char, long, float, int, double, long long.遍歷 variant 列表並對每一個 variant 應用 lambda 函式非常簡單(程式碼第15行到17行).藉助 typeid 函式,我便可以獲得 variant 的實際型別(程式碼第22行到24行).到這裡,我想你應該已經看出了程式碼中的訪問者模式, std::vector<std::variant> 就是我應用各種函式(即訪問者)的被訪問資料結構.
現在,我想將各個 variant 的元素求和.求和之前,我需要在編譯期確定所求和的結果型別,為此我使用了 std::common_type (程式碼第29行), std::common_type 可以給出 char, long, float, int, double, 和 long long 都可以進行隱式轉換的型別(double型別).程式碼中的 res{} 定義將 res(求和結果) 初始化為了 0.0,並在第33行到35行執行了真正的求和操作.我甚至使用訪問者動態的修改了 variant 中的元素(程式碼第40行).
程式的輸出如下.Visual C++ 中的執行時型別資訊(std::type_info)給出了非常易讀的型別名稱.