1. 程式人生 > >[譯]C++17,標準庫有哪些新變化?

[譯]C++17,標準庫有哪些新變化?

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

C++17 有許多新的標準庫變化,簡單起見,這篇文章只介紹了以下內容:std::string_view,標準模板庫中新新增的並行演算法,新的檔案系統庫,以及3個新的資料型別:std::any, std::optional, 和 std::variant.讓我們來了解一下其中的細節.

首先看看 std::string_view.

std::string_view

std::string_view 代表一個字串的非所有權引用(即不負責管理引用字串的生命週期),他表示的是一個字元序列(可以是 C++ 中的 string 或者 C風格的字串)的"檢視".C++17 中為不同的字元型別提供了四種 string_view :

std::string_view      std::basic_string_view<char>
std::wstring_view     std::basic_string_view<wchar_t>
std::u16string_view   std::basic_string_view<char16_t>
std::u32string_view   std::basic_string_view<char32_t>

你也許會有疑問:為什麼我們需要 std::string_view 呢(Google, LLVM 和 Bloomberg 甚至實現了自己的 string_view 版本)? .答案其實很簡單: 因為 std::string_view 可以高效的進行復制!

而高效的原因在於 std::string_view 的建立成本很低, 僅需要兩個資料:字元序列的指標以及字元序列的長度. std::string_view 以及他的3個"兄弟"型別(指 std::wstring_view, std::u16string_view 和 std::u32string_view)提供了和 std::string 一致的字串讀取介面,另外也新增了兩個方法:remove_prefix 和 remove_suffix.(譯註:譯文對作者的原始示例程式碼做了些許調整,原始程式碼請參看原文)

#include <iostream>
#include <string>
#include <string_view>

int main()
{
    std::string str = "   A lot of space";
	std::string_view strView = str;
	strView.remove_prefix(std::min(strView.find_first_not_of(" "), strView.size()));
	std::cout << "str      :  " << str << std::endl
		      << "strView  : " << strView << std::endl;

	std::cout << std::endl;

	char arr[] = { 'A', ' ', 'l', 'o', 't', ' ', 'o', 'f', ' ', 's', 'p', 'a', 'c', 'e', '\0', '\0', '\0' };
	std::string_view strView2(arr, sizeof arr);
	auto trimPos = strView2.find('\0');
	if (trimPos != strView2.npos) strView2.remove_suffix(strView2.size() - trimPos);
	std::cout << "arr     : " << arr << ", size=" << sizeof arr << std::endl
		      << "strView2: " << strView2 << ", size=" << strView2.size() << std::endl;
		      
    return 0;
}

示例程式碼應該沒有什麼令人驚訝的地方:第8行程式碼建立了引用 C++ string 的 std::string_view(strView變數), 而第16行程式碼中建立的 std::string_view(strView2變數) 引用的則是字元陣列.在第9行程式碼中,我們通過組合使用 remove_prefix 和 find_first_not_of 方法移除了 strView 的所有前導空格符,同樣在第21行程式碼中, 藉助 remove_suffix 方法, strView2 的所有尾隨"\0"符號也被移除了.

image

下面介紹的內容你應該更加熟悉.

Parallel algorithm of the Standard Template Library(標準模板庫中的並行演算法)

關於STL中並行演算法的介紹比較簡短: 標準庫中的 69 個演算法會提供序列,並行以及向量並行這3個版本.另外,新標準(C++17)也引入了 8 個(此處有誤,見後面譯註)新演算法.下面的示意圖示明瞭所有相關演算法的名字,其中新引入的演算法標為紅色,非新引入的演算法則為黑色.(譯註:圖中紅色標明的 for_each 並非是新演算法,所以實際C++17新引入的演算法只有7個)

image

演算法的介紹這麼多了,關於這個話題的進一步細節你可以看看我寫的另外一篇文章.

相比較演算法,檔案系統庫應該屬於全新的內容.

The filesystem library

新的檔案系統庫基於 boost::filesystem,並且檔案系統庫中的一些元件是可選的,這意味著並不是每一個檔案系統庫實現都支援標準定義的所有功能.例如, FAT-32 檔案系統便不支援符號連結.

檔案系統庫基於3個概念: 檔案(file), 檔名(file name) 以及 檔案路徑(path). file 可以是目錄,硬連結,符號連結或者常規檔案.path 則可以是絕對路徑或者相對路徑.

filesystem 提供了強大的讀取及操作檔案的介面,
你可以在cppreference.com上獲取到更多細節,下面的示例程式碼可以給你一些初步印象:

#include <fstream>
#include <iostream>
#include <string>
#include <filesystem>
namespace fs = std::filesystem;

int main()
{
	std::cout << "Current path: " << fs::current_path() << std::endl;

	std::string dir = "sandbox/a/b";
	fs::create_directories(dir);

	std::ofstream("sandbox/file1.txt");
	fs::path symPath = fs::current_path() /= "sandbox";
	symPath /= "syma";
	fs::create_symlink("a", symPath);

	std::cout << "fs::is_directory(dir): " << fs::is_directory(dir) << std::endl;
	std::cout << "fs::exists(symPath): " << fs::exists(symPath) << std::endl;
	std::cout << "fs::symlink(symPath): " << fs::is_symlink(symPath) << std::endl;

	for (auto& p : fs::recursive_directory_iterator("sandbox"))
	{
		std::cout << p.path() << std::endl;
	}
	fs::remove_all("sandbox");
	
	return 0;
}

第9行程式碼中的 fs::current_path() 方法可以返回當前工作目錄.你也可以使用
fs::create_directories 方法(程式碼第12行)建立層級目錄. fs::path 過載了 /= 操作符,藉助他我們可以方便的建立符號連結(第17行),你也可以使用檔案庫提供的介面來檢查檔案的各項屬性(19行到21行).23行的 fs::recursive_directory_iterator 功能非常強大,你可以使用他來遞迴的遍歷某個目錄,當然,你也可以使用 fs::remove_all 來刪除某個目錄(第27行).

程式碼的輸出如下:

image

新加入的資料型別 std::any, std::optional, 和 std::variant 都基於 boost程式庫.

std::any

如果你想建立一個可以包含任意型別元素的容器,那麼你就應該使用std::any,不過確切來說的話,std::any 並不是對任意型別都提供儲存支援,只有可複製的型別才能存放入 std::any.下面列一段簡短的示例程式碼:

#include <iostream>
#include <string>
#include <vector>
#include <any>

struct MyClass {};

int main() 
{
	std::cout << std::boolalpha;

	std::vector<std::any> anyVec { true, 2017, std::string("test"), 3.14, MyClass() };
	std::cout << "std::any_cast<bool>anyVec[0]: " << std::any_cast<bool>(anyVec[0]) << std::endl; // true
	int myInt = std::any_cast<int>(anyVec[1]);
	std::cout << "myInt: " << myInt << std::endl;                                    // 2017

	std::cout << std::endl;
	std::cout << "anyVec[0].type().name(): " << anyVec[0].type().name() << std::endl;             // b
	std::cout << "anyVec[1].type().name(): " << anyVec[1].type().name() << std::endl;             // i
	
	return 0;
}

示例程式碼的輸出已經在註釋中寫明瞭.程式碼第 12 行建立了一個 std::vectorstd::any,你必須使用 std::any_cast 來獲取其中的元素,如果你向 std::any_cast 傳遞了錯誤的資料型別,那麼就會產生轉型異常(std::bad_any_cast).你可以去cppreferenc.com獲取更多相關細節或者等待我之後的更多文章介紹.

std::any 可以儲存任意型別(譯註:這裡的任意型別指可複製的型別)的資料,而 std::optional 則支援儲存資料或者不儲存資料.

std::optional

std::optional 這裡就不做介紹了,在之前我寫的 Monads in C++ 中就已經介紹了這個單子(指std::optional).(譯註: 單子(Monad) 是函數語言程式設計程式設計的概念,簡單理解的話可以看看這裡)

我們再來看下 std::variant.

std::variant

std::variant 是一個型別安全的聯合體(union).一個 std::variant 例項儲存著其指定型別中某一型別的資料,並且 std::variant 的指定型別不能是引用型別,陣列型別以及 void 型別,不過 std::variant 可以指定重複的資料型別(譬如指定多個int). std::variant 預設會以其第一個指定型別進行初始化,這就要求該型別(第一個指定型別)必須支援預設建構函式,下面是一個基於cppreference.com的程式碼示例:

#include <variant>
#include <string>

int main() 
{
	std::variant<int, float> v, w;
	v = 12;                              // v contains int
	int i = std::get<int>(v);
	w = std::get<int>(v);
	w = std::get<0>(v);                  // same effect as the previous line
	w = v;                               // same effect as the previous line

	//std::get<double>(v);               // error: no double in [int, float]
	//std::get<3>(v);                    // error: valid index values are 0 and 1

	try 
	{
		float f = std::get<float>(w);    // w contains int, not float: will throw
	}
	catch (std::bad_variant_access&) 
	{
	}

	std::variant<std::string> v2("abc"); // converting constructors work when unambiguous
	v2 = "def";                          // converting assignment also works when unambiguous
	
	return 0;
}

第6行程式碼中我建立了兩個 std::variants 例項 v 和 w,他們的指定型別為 int 和 float,並且初始值為0(第一個指定型別 int 的預設初始值).第7行程式碼中我將整型12賦值給了v,後面我們可以通過 std::get(v) 來獲取該值.第9行到11行程式碼中,我使用了3種方式將v中的數值賦值給了w. std::variants 的使用自然也有一定的規則限制,你可以使用指定某一型別(第9行程式碼)或者指定某一索引(第10行程式碼)的方式來獲取 std::variants 的數值,但是指定的型別必須是唯一的,指定的索引也必須是有效的.第18行程式碼中我嘗試從 w 中獲取 float 型別資料,但是由於 w 目前包含 int 型別資料,所以會產生 std::bad_variant_access 異常.另外值得一提的是, std::variants 的建構函式以及賦值函式支援型別轉換(要求轉換沒有歧義),這也是第24行及25行程式碼中我可以使用C風格的字串直接初始化(或者賦值) std::variantstd::string 的原因.