1. 程式人生 > >[譯]C++17, 語言核心層有哪些新的變化?

[譯]C++17, 語言核心層有哪些新的變化?

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

C++11, C++14, 以及 C++17. 我猜你已經看出了其中的命名模式: 今年(2017)的晚些時候,我們便會迎來新的C++標準(C++17). 今年的3月份, C++17已經達到了標準草案階段. 在我深入新標準的細節之前, 讓我們先來總體瀏覽一下C++17.(譯註:作者的文章寫於2017年初,當時C++17標準仍未正式釋出)

讓我們首先來看下C++標準整體的(特性)時間線.

The big picture

這裡寫圖片描述

從 C++98 到 C++14,圖中只列出了較大的特性要點.圖中也缺少了關於 C++03 的特性描述, 因為C++03標準非常小,內容上更多是為了修復 C++98 的一些缺陷.如果你熟悉C++,那麼你一定知道 C++98(第一個C++標準) 和 C++11 是兩個非常大的C++標準, 但C++14,特別是C++03則是兩個小標準.

那麼 C++17 是大標準還是小標準呢?從我的觀點來看,答案其實挺簡單的: C++17 介於 C++14 和 C++11 之間,既不屬於大標準也不屬於小標準,至於原因,看看下面的說明吧.

概覽

C++17 在語言核心層標準庫方面都有很多新改動.我們首先來看下語言核心層.

語言核心層

fold expressions(摺疊表示式)

C++11 開始支援可變引數模板(即支援任意多數量引數的模板).其中任意數量的模板引數儲存在引數包(parameter pack)中.在C++17中,你可以使用二元運算子直接化簡(reduce)引數包:
(譯註:譯文對作者的原始示例程式碼做了些許調整,原始程式碼請參看

原文)

#include <iostream>

template<typename... Args>
bool all(Args... args)
{
	return (... && args);
}

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

	std::cout << "all(): " << all() << std::endl;
	std::cout << "all(true): " << all(true) << std::endl;
	std::cout << "all(true, true, true, false): " << all(true, true, true, false) << std::endl;

	std::cout << std::endl;

	return 0;
}

上述程式碼中(第6行)使用的二元運算子是邏輯與(&&).程式的輸出如下:

輸出圖

對於摺疊表示式我想說的就是這些,如果你想了解更多的細節,可以看看我之前的一篇關於摺疊表示式的文章.

我們繼續來看看編譯期的改動

constexpr if

constexpr if 可以實現原始碼的條件編譯.

template <typename T>
auto get_value(T t) 
{
	if constexpr (std::is_pointer_v<T>)
		return *t; // deduces return type to int for T = int*
	else
		return t;  // deduces return type to int for T = int
}

如果 T 是指標型別,那麼上述程式碼中的第5行分支就會被編譯,反之則編譯第7行的程式碼分支.這裡有兩個要點: 函式 get_value 有兩種不同的返回型別並且 if 語句的兩個分支都必須有效.

在C++17中, for 語句的語法同樣適用於 if 和 switch 語句了.

initializers in if and switch statements

現在你可以直接在 if 和 switch 語句中初始化變量了.

std::map<int, std::string> myMap;

if (auto result = myMap.insert(value); result.second) 
{
	useResult(result.first);
	// ...
}
else 
{
	// ...
} // result is automatically destroyed

初始化的變數僅在對應的 if 和 else 語句的作用域內有效,不會影響到外層作用域.

如果我們再結合使用一下C++17中新引入的結構化繫結宣告(structured binding declaration),那麼語法會更加優雅.

structured binding declaration(結構化繫結宣告)

藉助結構化繫結,我們可以直接將 std::tuple 或者某個結構的元素繫結到變數上去,讓我們用結構化繫結宣告來改寫一下之前的示例程式碼:

std::map<int, std::string> myMap;

if (auto[iter, succeeded] = myMap.insert(value); succeeded) 
{
	useIter(iter);
	// ...
}
else 
{
	// ...
} iter and succeded are automatically be destroyed

第3行的 auto [iter, succeeded] 自動建立了兩個變數(iter 和 succeeded),他們會在第 11 行程式碼執行中(離開if的作用域)被銷燬.

結構化繫結宣告可以簡化程式碼,建構函式的模板引數推導同樣也可以.

Template deduction of constructors(建構函式的模板引數推導)

一個函式模板可以通過傳遞的函式引數進行引數的型別推導,但這條規則對於一個特殊的函式模板卻不適用:類模板的建構函式.在 C++17 中,類模板的建構函式也能進行引數的型別推導了:

#include <iostream>

template <typename T>
void showMe(const T& t) 
{
	std::cout << t << std::endl;
}

template <typename T>
struct ShowMe 
{
	ShowMe(const T& t) 
	{
		std::cout << t << std::endl;
	}
};

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

	showMe(5.5);  // no need showMe<double>(5.5);
	showMe(5);    // no need showMe<int>(5);

	ShowMe(5.5);  // with C++17: no need ShowMe<double>(5.5);
	ShowMe(5);    // with C++17: no need ShowMe<int>(5);

	std::cout << std::endl;

	return 0;
}

22行和23行程式碼從C++第一個標準開始(C++98)便是合法的,但是25行及26行程式碼則只能在C++17中編譯通過,因為在C++17之前,你必須使用尖括號(<>)來指定需要例項化的類模板的型別引數.

除了功能特性,C++17中還有一些旨在提升程式碼執行效率的特性.

guaranteed copy elision

RVO是返回值優化(Return Value Optimisation)的簡稱,他的作用是允許編譯器移除一些不必要的複製操作,但RVO一直都只是一種可能優化步驟(並沒有標準規範,編譯器可以選擇進行RVO或者不進行RVO),C++17中通過定義 guaranteed copy elision 保證了這種優化的執行.

MyType func()
{
    return MyType{};         // no copy with C++17
}

MyType myType = func();    // no copy with C++17

在這幾行程式碼的執行中可能會發生2次不必要的複製操作.第1次發生在第3行,第2次則發生在第6行.但在C++17中,這2次多餘的複製操作都(保證)不會發生.

如果返回值有名稱,我們便稱他為NRVO(Named Return Value Optimization,命名返回值優化):

MyType func()
{
    MyType myVal;
    return myVal;            // one copy allowed 
}

MyType myType = func();    // no copy with C++17

上述程式碼(第4行)與之前程式碼的一個細微差別是:在C++17中,編譯器仍然可以執行一次 myVal 的複製操作(也可以不執行復制),但第7行程式碼仍然保證不會發生複製操作.

如果你不再需要某個特性,甚至於某個特性可能會造成"危險",那麼你就應該移除他.C++17中就移除了auto_ptr 和 trigraphs 這兩個語言特性.

移除 auto_ptr 和 trigraphs

auto_ptr

std::auto_ptr 是C++標準中第一個智慧指標,他的設計目的是為了正確的管理資源.但是他存在一個很大的缺陷: std::auto_ptr 可以進行復制(和賦值)操作,但內部執行的卻是移動(move)操作!(譯註:意為 std::auto_ptr 複製(和賦值)操作會改變源運算元的內部資料,因此其不能進行邏輯獨立的複製(和賦值)操作,也是因為這個原因, std::auto_ptr 不能作為標準庫容器的元素).正因為 std::auto_ptr 的這個缺陷, C++11 中作為替代引入了不可複製(只可移動)的 std::unique_ptr.

std::auto_ptr<int> ap1(new int(2011));
std::auto_ptr<int> ap2= ap1;              // OK     (1)

std::unique_ptr<int> up1(new int(2011));
std::unique_ptr<int> up2= up1;            // ERROR  (2)
std::unique_ptr<int> up3= std::move(up1); // OK     (3)
trigraphs(三字元組)

所謂三字元組(trigraphs),是指原始碼中由特定的3個字元組成的轉義字元序列(該轉義序列用以表達某個單字元),目的是解決一些鍵盤不能輸入某些特殊字元的問題.

C++ 中移除了三字元組(trigraphs),這意味著你不能使用C++17寫出下面這種"混亂"的程式碼了:

int main()
??<
  ??(??)??<??>();
??>

我猜你也許能看懂上面的程式碼,如果不能的話,你就必須把其中的三字元組(trigraphs)轉成對應的單字元了.

這裡寫圖片描述

如果你對著上面的表格進行了轉換,你會發現上面的程式碼實際上就是定義了一個就地執行(just-in-place)的 lambda 函式.

int main()
{
  []{}();
}

(譯註:文章中的不少說明涉及到了程式碼行號,但譯文中的示例程式碼並沒有行號顯示,原因是自己未找到markdown中原始碼顯示行號的簡易方法,有知道的朋友可以告訴一聲)