天天看點

《深入了解C++11》筆記(第四章. 新兵易學,老兵易用)

今天二刷《深入了解C++11》,就順帶把我在印象筆記的摘錄傳到CSND上,禁止轉載!!!

全部筆記連結:

  • 《深入了解C++11》筆記(第二章. 保證穩定性和相容性)
  • 《深入了解C++11》筆記(第三章. 通用為本,專用為末)
  • 《深入了解C++11》筆記(第四章. 新兵易學,老兵易用)
  • 《深入了解C++11》筆記(第五章. 提高類型安全)
  • 《深入了解C++11》筆記(第六章. 提高性能及操作硬體的能力)
  • 《深入了解C++11》筆記(第七章. 為改變思考方式而改變)
  • 《深入了解C++11》筆記(第八章. 融入實際的應用)

文章目錄

    • 新手易學,老兵易用
      • 1 右尖括号>的改進
      • 2 auto類型推導
        • 2.1 靜态類型、動态類型與類型推導
        • 2.2 auto優勢
        • 2.3 auto的使用細則
      • 3 decltype
        • 3.1 typeid與decltype
        • 3.2 decltype的應用
        • 3.3 decltype推導四規則
        • 3.4 cv限制符的繼承與備援的符号
      • 4 追蹤傳回類型
        • 4.1 追蹤傳回類型的引入
        • 4.2 使用追蹤傳回類型的函數
      • 5 基于範圍的for循環

新手易學,老兵易用

1 右尖括号>的改進

2 auto類型推導

2.1 靜态類型、動态類型與類型推導

  • 從技術上嚴格地講,靜态類型和動态類型的主要差別在于對變量進行類型檢查的時間點。對于所謂的靜态類型,類型檢查主要發生在編譯階段;而對于動态類型,類型檢查主要發生在運作階段。形如Python等語言中變量“拿來就用”的特性,則需要歸功于一個技術,即類型推導。
  • auto聲明變量的類型必須由編譯器在編譯時期推導而得。
  • auto聲明的變量必須被初始化,以使編譯器能夠從其初始化表達式中推導出其類型。從這個意義上來講,auto并非一種“類型”聲明,而是一個類型聲明時的“占位符”,編譯器在編譯時期會将auto替代為變量實際的類型。

2.2 auto優勢

  • 最大優勢就是在擁有初始化表達式的複雜類型變量聲明時簡化代碼。
  • 第二個優勢則在于可以免除程式員在一些類型聲明時的麻煩,或者避免一些在類型聲明時的錯誤。
  • 第三個優點就是其“自适應”性能夠在一定程度上支援泛型的程式設計。
#define Max1(a, b) ((a) > (b)) ? (a) : (b)
#define Max2(a, b) ({ \
        auto _a = (a); \
        auto _b = (b); \
        (_a > _b) ? _a: _b; })
int main() {
    int m1 = Max1(1*2*3*4, 5+6+7+8);
    int m2 = Max2(1*2*3*4, 5+6+7+8);
}
// 編譯選項:g++ -std=c++11 4-2-8.cpp
           

性能Max1 < Max2,因為 Max2少計算一次a或者b的值

這個與我不願意寫複雜的三元運算符表達式的願意驚人的一緻

2.3 auto的使用細則

  • 如果要使得auto聲明的變量是另一個變量的引用,則必須使用auto &,而對于指針,auto 與 auto*一樣。
  • C++11标準規定auto可以與cv限制符一起使用,不過聲明為auto的變量并不能從其初始化表達式中“帶走”cv限制符(const && volatile)。
double foo();
float * bar();
const auto a = foo();         // a: const double
const auto & b = foo();      // b: const double&
volatile auto * c = bar();   // c: volatile float*
auto d = a;                      // d: double
auto & e = a;                   // e: const double &
auto f = c;                      // f: float *
volatile auto & g = c;        // g: volatile float * &
// 編譯選項:g++ -std=c++11 4-2-10.cpp
           
這裡的例外還是引用,可以看出,聲明為引用的變量e,g都保持了其引用的對象的相同屬性(事實上,對于指針也是一樣的)。
  • 用auto來聲明多個變量類型時,隻有第一個變量用于auto的類型推導,然後推導出來的資料類型被作用于其他的變量
  • auto的使用限制
#include <vector>
using namespace std;
void fun(auto x =1){}  // 1: auto函數參數,無法通過編譯
struct str{
    auto var = 10;    // 2: auto非靜态成員變量,無法通過編譯
};
int main() {
    char x[3];
    auto y = x;
    auto z[3] = x; // 3: auto數組,無法通過編譯
    // 4: auto模闆參數(執行個體化時),無法通過編譯
    vector<auto> v = {1};
}
// 編譯選項:g++ -std=c++11 4-2-13.cpp
           
auto是不能做形參的類型的。如果程式員需要泛型的參數,還是需要求助于模闆。
對于結構體來說,非靜态成員變量的類型不能是auto的。
聲明auto數組。
在執行個體化模闆的時候使用auto作為模闆參數.
  • 此外,程式員還應該注意,由于為了避免和C++98中auto的含義發生混淆,C++11隻保留auto作為類型訓示符的用法,以下的語句在C++98和C語言中都是合法的,但在C++11中,編譯器則會報錯。
    auto int i = 1;
               

3 decltype

3.1 typeid與decltype

  • RTTI(運作時類型識别)
RTTI的機制是為每個類型産生一個type_info類型的資料,程式員可以在程式中使用typeid随時查詢一個變量的類型,typeid就會傳回變量相應的type_info資料。而type_info的name成員函數可以傳回類型的名字。而在C++11中,又增加了hash_code這個成員函數,傳回該類型唯一的哈希值,以供程式員對變量的類型随時進行比較。
#include <iostream>
#include <typeinfo>
using namespace std;
class White{};
class Black{};
int main() {
    White a;
    Black b;
    cout << typeid(a).name() << endl;    // 5White
    cout << typeid(b).name() << endl;    // 5Black
    White c;
    bool a_b_sametype = (typeid(a).hash_code() == typeid(b).hash_code());
      bool a_c_sametype = (typeid(a).hash_code() == typeid(c).hash_code());
      cout << "Same type? " << endl;
      cout << "A and B? " << (int)a_b_sametype << endl;    // 0
      cout << "A and C? " << (int)a_c_sametype << endl;    // 1
  }
  // 編譯選項:g++ -std=c++11 4-3-1.cpp
           
is_same模闆函數的成員類型value在編譯時得到資訊,hash_code是運作時得到的資訊
  • RTTI會帶來一些運作時的開銷,是以一些編譯器會讓使用者選擇性地關閉該特性(比如XL C/C++編譯器的-qnortti,GCC的選項-fno-rttion,或者微軟編譯器選項/GR-)。且很多時候,運作時才确定出類型對于程式員來說為時過晚,程式員更多需要的是在編譯時期确定出類型(标準庫中非常常見)。而通常程式員是要使用這樣的類型而不是識别該類型,是以RTTI無法滿足需求。
  • decltype的類型推導并不是像auto一樣是從變量聲明的初始化表達式獲得變量的類型,decltype總是以一個普通的表達式為參數,傳回該表達式的類型。而與auto相同的是,作為一個類型訓示符,decltype可以将獲得的類型來定義另外一個變量。與auto相同,decltype類型推導也是在編譯時進行的。
#include <typeinfo>
#include <iostream>
using namespace std;
int main() {
    int i;
    decltype(i) j = 0;
    cout << typeid(j).name() << endl;    // 列印出"i", g++表示int
    float a;
    double b;
    decltype(a + b) c;
    cout << typeid(c).name() << endl;    // 列印出"d", g++表示double
}
// 編譯選項:g++ -std=c++11 4-3-2.cpp
           

3.2 decltype的應用

  • decltype與typdef/using的合用
using size_t = decltype(sizeof(0));
using ptrdiff_t = decltype((int*)0- (int*)0);
using nullptr_t = decltype(nullptr);
           
颠覆了之前類型拓展需要将擴充類型“映射”到基本類型的正常做法。
  • 重用匿名類型
enum class{K1, K2, K3}anon_e;    // 匿名的強類型枚舉
union {
    decltype(anon_e) key;
    char* name;
}anon_u;     // 匿名的union聯合體
struct {
    int d;
    decltype(anon_u) id;
}anon_s[100];    // 匿名的struct數組
int main() {
    decltype(anon_s) as;
    as[0].id.key = decltype(anon_e)::K1;    // 引用匿名強類型枚舉中的值
}
// 編譯選項:g++ -std=c++11 4-3-4.cpp
           
匿名一般都有匿名理由,一般程式員都不希望匿名後的類型被重用。這裡的decltype隻是提供了一種文法上的可能。
  • 可以适當擴大模闆泛型的能力
// s的類型被聲明為decltype(t1 + t2)
template <typename T1, typename T2>
void Sum(T1 & t1, T2 & t2, decltype(t1 + t2) & s) {
    s = t1 + t2;
}
int main() {
    int a = 3;
    long b = 5;
    float c = 1.0f, d = 2.3f;
    long e;
    float f;
    Sum(a, b, e);    // s的類型被推導為long
    Sum(c, d, f);    // s的類型被推導為float
}
// 編譯選項:g++ -std=c++11 4-3-5.cpp
           

不過這裡還是有一定的限制,我們可以看到傳回值的類型必須一開始就被指定,程式員必須清楚Sum運算的結果使用什麼樣的類型來存儲是合适的,這在一些泛型程式設計中依然不能滿足要求。解決的方法是結合decltype與auto關鍵字,使用追蹤傳回類型的函數定義來使得編譯器對函數傳回值進行推導。

事實上,decltype一個最大的用途就是用在追蹤傳回類型的函數中

  • 注意
  1. 執行個體1
#include <map>
using namespace std;
int hash(char*);
map<char*, decltype(hash)> dict_key;     // 無法通過編譯
map<char*, decltype(hash(nullptr))> dict_key1;
// 編譯選項:g++ -c -std=c++11 4-3-7.cpp
           
decltype隻能接受表達式做參數,像函數名做參數的表達式decltype(hash)是無法通過編譯的
  1. 執行個體2
#include <type_traits>
using namespace std;
typedef double (*func)();
int main() {
    result_of<func()>::type f;    // 由func()推導其結果類型
}
// 編譯選項:g++ -std=c++11 4-3-8.cpp
           
f的類型最終被推導為double,而result_of并沒有真正調用func()這個函數,一切都是因為底層實作了decltype。

result_of的一個可能的實作方式如下:

template<class>
struct result_of;
template<class F, class... ArgTypes>
struct result_of<F(ArgTypes...)>
{
    typedef decltype(
                      std::declval<F>()(std::declval<ArgTypes>()...)
                      ) type;
};
           

3.3 decltype推導四規則

  • 具體地,當程式員用decltype(e)來擷取類型時,編譯器将依序判斷以下四規則:
    1. 如果e是一個沒有帶括号的标記符表達式(id-expression)或者類成員通路表達式,那麼decltype(e)就是e所命名的實體的類型。此外,如果e是一個被重載的函數,則會導緻編譯時錯誤。
    2. 否則,假設e的類型是T,如果e是一個将亡值(xvalue),那麼decltype(e)為T&&。
    3. 否則,假設e的類型是T,如果e是一個左值,則decltype(e)為T&。
    4. 否則,假設e的類型是T,則decltype(e)為T。
  • 标記符表達式(id-expression)

    基本上,所有除去關鍵字、字面量等編譯器需要使用的标記之外的程式員自定義的标記(token)都可以是标記符(identifier)。

    單個标記符對應的表達式就是标記符表達式。

  • 執行個體
int i = 4;
int arr[5] = {0};
int *ptr = arr;
struct S { double d; } s;
void Overloaded(int);
void Overloaded(char);       // 重載的函數
int && RvalRef();
const bool Func(int);
// 規則1: 單個标記符表達式以及通路類成員,推導為本類型
decltype(arr) var1;              // int[5], 标記符表達式
decltype(ptr) var2;              // int*, 标記符表達式
decltype(s.d) var4;              // double, 成員通路表達式
decltype(Overloaded) var5;      // 無法通過編譯,是個重載的函數
// 規則2: 将亡值,推導為類型的右值引用
decltype(RvalRef()) var6 = 1;   // int&&
// 規則3: 左值,推導為類型的引用
decltype(true ? i : i) var7 = i;     // int&, 三元運算符,這裡傳回一個i的左值
decltype((i)) var8 = i;                // int&, 帶圓括号的左值
decltype(++i) var9 = i;                // int&, ++i傳回i的左值
decltype(arr[3]) var10 = i;           // int& []操作傳回左值
decltype(*ptr)  var11 = i;            // int& *操作傳回左值
decltype("lval") var12 = "lval";     // const char(&)[9], 字元串字面常量為左值
// 規則4:以上都不是,推導為本類型
decltype(1) var13;                 // int, 除字元串外字面常量為右值
decltype(i++) var14;              // int, i++傳回右值
decltype((Func(1))) var15;       // const bool, 圓括号可以忽略
// 編譯選項:g++ -std=c++11-c 4-3-10.cpp
           
  • 輔助判斷工具
is_lvalue_reference && is_rvalue_reference
#include <type_traits>
#include <iostream>
using namespace std;
int i = 4;
int arr[5] = {0};
int *ptr = arr;
int && RvalRef();
int main(){
    cout << is_rvalue_reference<decltype(RvalRef())>::value << endl;       // 1
    cout << is_lvalue_reference<decltype(true ? i : i)>::value << endl; // 1
    cout << is_lvalue_reference<decltype((i))>::value << endl;             // 1
    cout << is_lvalue_reference<decltype(++i)>::value << endl;             // 1
    cout << is_lvalue_reference<decltype(arr[3])>::value << endl;          // 1
    cout << is_lvalue_reference<decltype(*ptr)>::value << endl;            // 1
    cout << is_lvalue_reference<decltype("lval")>::value << endl;          // 1
    cout << is_lvalue_reference<decltype(i++)>::value << endl;             // 0
    cout << is_rvalue_reference<decltype(i++)>::value << endl;             // 0
}
// 編譯選項:g++ -std=c++11 4-3-11.cpp
           

3.4 cv限制符的繼承與備援的符号

  • 與auto類型推導時不能“帶走”cv限制符不同,decltype是能夠“帶走”表達式的cv限制符的。不過,如果對象的定義中有const或volatile限制符,使用decltype進行推導時,其成員不會繼承const或volatile限制符。
#include <type_traits>
#include <iostream>
using namespace std;
const int ic = 0;
volatile int iv;
struct S { int i; };
const S a = {0};
volatile S b;
volatile S* p = &b;
int main() {
    cout << is_const<decltype(ic)>::value << endl;        // 1
    cout << is_volatile<decltype(iv)>::value << endl;    // 1
    cout << is_const<decltype(a)>::value << endl;         // 1
    cout << is_volatile<decltype(b)>::value << endl;     // 1
    cout << is_const<decltype(a.i)>::value << endl;      // 0, 成員不是const
    cout << is_volatile<decltype(p->i)>::value << endl; // 0, 成員不是volatile
}
// 編譯選項:g++ -std=c++11 4-3-12.cpp
           
  • 而與auto相同的,decltype從表達式推導出類型後,進行類型定義時,也會允許一些備援的符号。比如cv限制符以及引用符号&,通常情況下,如果推導出的類型已經有了這些屬性,備援的符号則會被忽略。
#include <type_traits>
#include <iostream>
using namespace std;
int i = 1;
int & j = i;
int * p = &i;
const int k = 1;
int main() {
    decltype(i) & var1 = i;
    decltype(j) & var2 = i;      // 備援的&, 被忽略
    cout << is_lvalue_reference<decltype(var1)>::value << endl;// 1, 是左值引用
    cout << is_rvalue_reference<decltype(var2)>::value << endl;// 0, 不是右值引用
    cout << is_lvalue_reference<decltype(var2)>::value << endl;// 1, 是左值引用
    decltype(p)* var3 = &i;      // 無法通過編譯
    decltype(p)* var3 = &p;      // var3的類型是int**
    auto* v3 = p;                  // v3的類型是int*
    v3 = &i;
    const decltype(k) var4 = 1; // 備援的const,被忽略
    }
    // 編譯選項:g++ -std=c++11 4-3-13.cpp
           
這裡特别要注意的是decltype§*的情況。可以看到,在定義var3變量的時候,由于p的類型是int*,是以var3被定義為了int**類型。這跟auto聲明中,*也可以是備援的不同。在decltype後的*号,并不會被編譯器忽略。

4 追蹤傳回類型

4.1 追蹤傳回類型的引入

  • SUM函數第一版
template <typename T1, typename T2>
        double Sum(T1 & t1, T2 & t2) {
            auto s = t1 + t2;    // s的類型會在模闆執行個體化時被推導出來
            return s;
        }
        int main() {
            int a = 3;
            long b = 5;
            float c = 1.0f, d = 2.3f;
            auto e = Sum<int ,long>(a, b);       // s的類型被推導為long
            auto f = Sum<float,float>(c, d);     // s的類型被推導為float
        }
        // 編譯選項:g++ -std=c++11 4-2-7.cpp
           
  • SUM函數第二版
// s的類型被聲明為decltype(t1 + t2)
        template <typename T1, typename T2>
        void Sum(T1 & t1, T2 & t2, decltype(t1 + t2) & s) {
            s = t1 + t2;
        }
        int main() {
            int a = 3;
            long b = 5;
            float c = 1.0f, d = 2.3f;
            long e;
            float f;
            Sum(a, b, e);    // s的類型被推導為long
            Sum(c, d, f);    // s的類型被推導為float
        }
        // 編譯選項:g++ -std=c++11 4-3-5.cpp
           
  • SUM函數第三版

錯誤的樣例:

template <typename T1, typename T2>
        decltype(t1 + t2) Sum(T1 & t1, T2 & t2) {
            return t1 + t2;
        }
           
錯誤原因:編譯器在推導decltype(t1 + t2)時的,表達式中的t1和t2都未聲明(雖然它們近在咫尺,編譯器卻隻會從左往右地讀入符号)。

正确的樣例:

template  < typename T1,  typename T2 >
        auto Sum(T1 & t1, T2 & t2)  -> decltype ( t1 + t2) {
            return t1 + t2;
        }
           
  • C++11引入新文法:追蹤傳回類型,來聲明和定義這樣的函數。原本函數傳回值的位置由auto關鍵字占據。
  • auto 和 ->return_type 構成了追蹤傳回類型函數的兩個基本元素。

4.2 使用追蹤傳回類型的函數

  • 簡化函數的定義,提高代碼的可讀性。這種情況常見于函數指針中。
#include <type_traits>
#include <iostream>
using namespace std;
// 有的時候,你會發現這是面試題
int (*(*pf())())() {
  return nullptr;
}
// auto (*)() -> int(*) () 一個傳回函數指針的函數(假設為a函數)
// auto pf1() -> auto (*)() -> int (*)() 一個傳回a函數的指針的函數
auto pf1() -> auto (*)() -> int (*)() {
  return nullptr;
}
int main() {
  cout << is_same<decltype(pf), decltype(pf1)>::value << endl;     // 1
}
// 編譯選項:g++ -std=c++11 4-4-3.cpp
           

//從外往裡讀

int(*(*pf())())()

int(*)() (*pf())()

(int(*)() (*)()) pf()

//還是用function對象吧

int(*(*pf())())()

int(*)() (*pf())()

function (*pf())()

function ()() pf()

function()> pf()

  • 廣泛應用在轉發函數中
#include <iostream>
using namespace std;
double foo(int a) {
    return (double)a + 0.1;
}
int foo(double b) {
    return (int)b;
}
template <class T>
    auto Forward(T t) -> decltype(foo(t)){
    return foo(t);
}
int main(){
    cout << Forward(2) << endl;      // 2.1
    cout << Forward(0.5) << endl;    // 0
}
// 編譯選項:g++ -std=c++11 4-4-4.cpp
           
沒有傳回值的函數也可以被聲明為追蹤傳回類型,程式員隻需要将傳回類型聲明為void即可。

5 基于範圍的for循環

for (auto e: arr)
            cout << e << '\t';
           
  • 能否使用基于範圍的for循環,必須依賴于一些條件
    • for循環的疊代範圍是可确定的

      對于類來說,如果該類有begin和end函數,那麼begin和end之間就是for循環疊代的範圍。對于數組而言,就是數組的第一個和最後一個元素間的範圍。

    • 基于範圍的for循環還要求疊代的對象實作++和==等操作符。

      對于标準庫中的容器,如string、array、vector、deque、list、queue、map、set等,不會有問題,因為标準庫總是保證其容器定義了相關的操作。普通的已知長度的數組也不會有問題。而使用者自己寫的類,則需要自行提供相關操作。

  • 反例
#include <iostream>
using namespace std;
int func(int a[]) {
    for (auto e: a)
        cout << e;
}
int main() {
    int arr[] = {1, 2, 3, 4, 5};
    func(arr);
}
// 編譯選項:g++ -std=c++11 4-5-4.cpp
           
上述代碼會報錯,因為作為參數傳遞而來的數組a的範圍不能确定,是以也就不能使用基于範圍循環for循環對其進行疊代的操作。

繼續閱讀