14.C++11提高效能及操作硬體的能力
1. 常量表達式
1.1 執行時常量性和編譯時常量性
//執行時常量
const int getConst(){return 1;}
大多數情況下, const 描述的都是一些 “執行時常量性”的概念,即具有執行時資料的不可更改性。不過有時候我們需要的卻是編譯時期的常量性,這是 const 無法保證的。
//c 風格的巨集,簡單粗暴
#define getConst 1
//c++11 中的常量表達式
constexpr getCosnt(){return 1;}
c 風格的巨集定義和 c++11 提供的 constexpr 關鍵字,都可以在編譯期對 getConst 進行計算。
1.2 常量表達式函式
1.3 常量表達式值
使用 constexpr 宣告的資料最常被問起的問題是,下面兩條語句有什麼區別?
const int i = 1;
constexpr int j = 1;
事實上,兩者在大多數情況下是沒有區別的。不過有一點是肯定的,就是如果 i 在全域性名字空間中,編譯器一定會為 i 產生資料。而對於 j ,如果不是程式碼中顯式地使用了它的地址,編譯器可以選擇不為它生成資料,而僅將其當做編譯時期的值。
2. 變長模板
2.1 概述
在一些情況下,類也需要不定長度的模板引數。最為典型的就是 c++11 標準庫中的 tuple 類模板。如果讀者熟悉 c++98 中的 pair 類模板的話,那麼理解 tuple 也就不困難了。具體來講, pair 是兩個不同型別的資料的集合。比如 pair<int, double> 就能夠容納 int 型別和 double 型別的兩種資料。一些如 std::map 的標準庫容器,其成員就需要時類模板 pair 的。在 c++11 中, tuple 是 pair 類的一種更為泛化的表現形式。比起 pair, tuple 是可以接受任意多個不同型別的元素的集合。比如我們可以通過:
std::tuple<double, char, std::string> collections;
來宣告一個 tuple 模板類。該 collections 變數可以容納 double、char、std::string 三種類型的資料。當然,讀者還可以用更多的引數來宣告 collection, 因為 tuple 可以接受任意多的引數。此外,和 pair 類似地,我們可以更為簡單的使用 c++11 的模板函式 make_tuple 來創造一個 tuple 模板型別。
std::make_tuple(9.8, 'g', 'gravity');
3.原子型別與原子操作
3.1 並行程式設計、多執行緒與 c++11
常見的並行程式設計有多種模型,如共享記憶體、多執行緒、訊息傳遞等。不過從實用性上講,多執行緒模型往往具有較大的優勢。多執行緒模型允許同一時間有多個處理器單元執行同一程序中的程式碼部分,而通過分離的棧空間和共享的資料區及堆疊空間,執行緒可以擁有獨立的執行狀態以及進行快速的資料共享。
c++11 標準的一個相當大的變化就是引入了多執行緒的支援。這使得 c/c++ 語言在進行執行緒程式設計時,不必依賴第三方庫和標準。而 c/c++ 對執行緒的支援,一個最為重要的部分,就是在原子操作中引入原子型別的概念。
3.2 原子操作與 c++11 原子型別
所謂原子操作,就是多執行緒程式中 ”最小的且不可並行化的“ 操作。通常對一個共享資源的操作是原子操作的話,意味著多個執行緒訪問該資源時,有且僅有唯一一個執行緒在對這個資源進行操作。那麼從執行緒(處理器)的角度來看,其他執行緒就不能夠在本執行緒對資源訪問期間對資源進行操作,因此原子操作對於多執行緒而言,就不會發生有別於單執行緒程式的以外狀況。
通常情況下,原子操作都是通過 “互斥” 的訪問來保證的。實現互斥通常需要平臺相關的特殊指令,這在 c++11 標準之前,常常意味著需要在 c/c++ 程式碼中嵌入內聯彙編程式碼。對程式設計師來講,就必須瞭解平臺上與同步相關的彙編指令。當然,如果只是想實現粗粒度的互斥,藉助 POSIX 標準的 phtread 庫中的互斥鎖(mutex)也可以做到(先定義一個 mutex,然後訪問共享資源前加鎖,訪問完共享資源釋放鎖)。
c++11中,通過對並行程式設計更為良好的抽象,要實現同樣的功能就簡單了很多。
#include <thread>
#include <atomic>
#include <functional>
atomic_llong total{0};
long long total1{ 0 };
auto func1 = []()
{
for (long long i =0; i < 100000000LL; ++i)
{
total += i;
total1 += i;
}
};
void testAtomic()
{
cout << "enter testAtomic()...................................................................." << endl;
thread t1(func1);
thread t2(func1);
t1.join();
t2.join();
cout << "total: " << total << endl; //9999999900000000
cout << "total1: " << total1 << endl; // != 9999999900000000 並且每次執行結果不一致
cout << "return from testAtomic()...................................................................." << endl;
}
上例中,我們將變數 total 定義為一個 “原子資料型別”:atomic_llong, 該型別長度等同於 c++11 中的內建型別 long long。 在 c++11 中,程式設計師不需要為原子資料型別顯式的宣告互斥鎖或呼叫加鎖、解鎖的 API,執行緒就能夠對變數 total 互斥地進行訪問。
相比於基於 c 以及過程程式設計的 pthread "原子操作 API" 而言, c++11 對於 ”原子操作“ 概念的抽象遵從了面向物件的思想 —— c++11 標準定義的都是所謂的 ”原子型別“。而傳統意義上所謂的 ”原子操作“,則抽象為針對於這些原子型別的從操作(事實上,是原子型別的成員函式,稍後解釋)。直觀地看,編譯器可以保證原子型別線上程間被互斥地訪問。這樣設計,從並行程式設計的角度看,是由於需要同步的總是資料而不是程式碼,因此 c++11 對資料進行抽象,會有利於產生行為更為良好的並行程式碼。而進一步地,一些瑣碎的概念,比如互斥鎖、臨界區則可以被 c++11 的抽象所掩蓋,因此並行程式碼的編寫也會變得更加簡單。