1. 程式人生 > >[譯]C++17,optional, any, 和 variant 的更多細節

[譯]C++17,optional, any, 和 variant 的更多細節

看到一個介紹 C++17 的系列博文(原文),有十來篇的樣子,覺得挺好,看看有時間能不能都簡單翻譯一下,這是第六篇~

std::optional, std::any, 和 std::variant 有一個共同特點:他們都支援就地構造.另外的,std::variant 還支援訪問者模式.

首先,我們要了解一下這3種資料型別的功能作用.

  • std::optional 是一種可能包含也可能不包含某一型別物件的型別.
  • std::variant 是一種型別安全的聯合體
  • std::any 是一種可以包含任意型別(指可複製型別)物件的型別

我在之前的

文章中講解了這3個數據型別的一些細節,不瞭解的朋友可以先去看看,相關內容這裡就不再贅述了.

圖

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)給出了非常易讀的型別名稱.

圖