C++新語法特性

0. 背景

最近在學習過程中,不小心就發現一些不認識的C++語法和技巧,一查才發現我的認知已落後許久,隨着C++的發展,很多新奇的高級編程語言技巧都已經被C++吸納,而我的認知還一直在c++98停滯不前,慚愧。本章收集了一些比較常見的場景和技巧,記錄學習筆記,持續更新中…

auto – C++11

auto可以在聲明變量的時候根據變量初始值的類型自動爲此變量選擇匹配的類型,類似的關鍵字還有decltype,是一個萬能變量類型符號,其自動類型推斷髮生在編譯期,所以使用auto並不會造成程序運行時效率的降低,極大地方便代碼編寫。

使用時,可以把auto當做一個特殊的萬能變量類型,但它並不是一個真正的類型,因此有:

int s = 123;
const int c = 456;
int& rs = s;
auto  p1 = &s;      // p1爲int*類型,auto相當於int*
auto* p2 = &s;      // p2爲int*類型, auto相當於int
auto  p3 = c;       // p3爲int類型,auto相當於int
auto  p4 = &c;      // p4爲const int*類型,auto相當於const int*
auto* p5 = &c;      // p5爲const int*類型,auto相當於const int
auto  p6 = rs;      // p6爲int類型,auto相當於int
// sizeof(auto);    // 非法,auto非真正的類型

我原本以爲auto在程序預處理之後會被編譯器替換爲真正的類型,但是在Visual Studio編譯後查看對應的.i文件發現,auto還是那個auto,並沒有被預處理掉,推測auto作爲關鍵字並不會在編譯後替換,而是真正在程序後續編譯鏈接中起作用了。

decltype – C++11

decltype推斷出表達式的類型,繼而直接使用該類型。

int s = 123;
int& rs = s;
decltype(s)         s1;         // 類似於int
decltype(s+1)       s2;         // 類似於int
const decltype(s)   s3 = 2;     // 類似於int,定義爲const int類型
decltype(s)*        s4;         // 類似於int,定義爲int*類型
decltype(rs)        s5 = s1;    // 類似於int&,定義爲int&類型
decltype(rs+1)*     s6;         // 類似於int
decltype((s))       s7 = s;     // 類似於int&,定義爲int&類型 <- 一個表達式加上一對括號, 就變成了引用類型
decltype((rs))      s8 = s;     // 類似於int&,定義爲int&類型 <- 引用表達式再加上一對括號, 還是引用類型
decltype(*s4)       s9 = s;     // 類似於int&,定義爲int& <- 對指針的解引用操作返回的是引用類型

using

  • 對命名空間的 using 指令及對命名空間成員的 using 聲明

  • 對類成員的 using 聲明

    • 於類定義

    using 聲明引入基類成員到派生類定義,例如暴露基類的受保護成員爲派生類的公開成員。此情況下 nested-name-specifier 必須指名被定義的基類。若名稱是基類的重載成員函數之名,則引入帶該名稱的所有基類成員函數。若派生類已有同名、參數列表及限定的成員,則派生類成員隱藏或覆寫引入自基類的成員

  • 類型別名與別名模板聲明 (C++11 起):類型別名是指代先前定義類型的名稱(同typedef),但其語法表達更清晰易懂

    // 使用using
    using UA = int;
    UA a1;
    
    // 使用typedef
    typedef int TA;
    TA a2;
    

SFINAE

SFINAE是英文Substitution failure is not an error的縮寫,意思是匹配失敗不是錯誤。比較官方的解釋爲:
當調用模板函數時編譯器會根據傳入參數推導最合適的模板函數,在這個推導過程中如果某一個或者某幾個模板函數推導出來是編譯無法通過的,只要有一個可以正確推導出來,那麼那幾個推導得到的可能產生編譯錯誤的模板函數並不會引發編譯錯誤

即調用某部分代碼時,只要能找到匹配的模版定義,那麼其他一個或多個匹配模版代碼推導即使編譯有誤也不會報錯。

其作用是當我們在程序編譯時期進行模板特化的時候,會去選擇那個正確的模板,避免失敗,此特性可以允許編譯器在編譯期間根據不同的對象特點匹配其正確的功能代碼來執行。

例如代碼<轉藍色小藥丸>:

struct X {
  typedef int type;
};

struct Y {
  typedef int type2;
};

template <typename T> void foo(typename T::type);    // Foo0
template <typename T> void foo(typename T::type2);   // Foo1
template <typename T> void foo(T);                   // Foo2

void callFoo() {
   foo<X>(5);    // Foo0: Succeed, Foo1: Failed,  Foo2: Failed
   foo<Y>(10);   // Foo0: Failed,  Foo1: Succeed, Foo2: Failed
   foo<int>(15); // Foo0: Failed,  Foo1: Failed,  Foo2: Succeed
}

在這個例子中,當我們指定foo<Y>的時候,substitution就開始工作了,而且會同時工作在三個不同的foo簽名上。如果我們僅僅因爲Y沒有type,就在匹配Foo0時宣佈出錯,那顯然是武斷的,因爲我們起碼能保證,也希望將這個函數匹配到Foo1上。

實際上,std/boost庫中的enable_if也是借用了這個原理。

我們來看enable_if的一個應用:假設我們有兩個不同類型的counter,一種counter是普通的整數類型,另外一種counter是一個複雜對象,它有一個成員叫做increase。現在,我們想把這兩種類型的counter封裝一個統一的調用:inc_counter。那麼,我們直覺會簡單粗暴的寫出下面的代碼:

struct ICounter {
  virtual void increase() = 0;
  virtual ~ICounter() {}
};

struct Counter: public ICounter {
   void increase() override {
      // Implements
   }
};

template <typename T>
void inc_counter(T& counterObj) {
  counterObj.increase();
}

template <typename T>
void inc_counter(T& intTypeCounter){
  ++intTypeCounter;
}

void doSomething() {
  Counter cntObj;
  uint32_t cntUI32;

  // blah blah blah
  inc_counter(cntObj);
  inc_counter(cntUI32);
}

我們非常希望它可以如我們所願的work——因爲其實我們是知道對於任何一個調用,兩個inc_counter只有一個是正常工作的。“有且唯一”,我們理應當期望編譯器能夠挑出那個唯一來。

可惜編譯器做不到這一點。首先,它就告訴我們,這兩個簽名其實是一模一樣的,但是編譯器是禁止出現相同簽名的函數的,因此會導致redefinition。

template <typename T> void inc_counter(T& counterObj);
template <typename T> void inc_counter(T& intTypeCounter);

所以我們要藉助於enable_if這個T對於不同的實例做個限定:

template <typename T> void inc_counter(
  T& counterObj, 
  typename std::enable_if<
    is_base_of<T, ICounter>::value
  >::type* = nullptr );

template <typename T> void inc_counter(
  T& counterInt,
  typename std::enable_if<
    std::is_integral<T>::value
  >::type* = nullptr );

關於這個enable_if是怎麼工作的,語法爲什麼這麼醜,我來解釋一下:

首先,substitution只有在推斷函數類型的時候,纔會起作用。推斷函數類型需要參數的類型,所以,typename std::enable_if<std::is_integral<T>::value>::type這麼一長串代碼,就是爲了讓enable_if參與到函數類型中;

其次,is_integral<T>::value返回一個布爾類型的編譯器常數,告訴我們它是或者不是一個integral,enable_if<C>的作用就是,如果這個C值爲True,那麼type就會被推斷成一個void或者是別的什麼類型,讓整個函數匹配後的類型變成void inc_counter<int>(int & counterInt, void* dummy = nullptr);如果這個值爲False,那麼enable_if<false>這個特化形式中,壓根就沒有這個::type,於是substitution就失敗了 —— 所以這個函數原型根本就不會被產生出來。

所以我們能保證,無論對於int還是counter類型的實例,我們都只有一個函數原型是通過了substitution —— 這樣就保證了它的“有且唯一”,編譯器也不會因爲你某個替換失敗而無視成功的那個實例。

enable_if

enable_if一般實現方法如下:

template <bool, typename T = void>
struct enable_if
{};

template <typename T>
struct enable_if<true, T> {
  typedef T type;
};

即當不滿足條件時不會定義類型type導致推導失敗,不會產生相對應的模版代碼,否則會返回對應模版類型,可使用typename 使用該類型做飯返回值或參數定義。

此元函數是活用SFINAE,基於類型特性條件性地從重載決議移除函數,並對不同類型特性提供分離的函數重載與特化的便利方法。 std::enable_if 可用作額外的函數參數(不可應用於運算符重載)、返回類型(不可應用於構造函數與析構函數),或類模板或函數模板形參。

該方法是在C++ 11引進的,能夠在編譯期間根據條件去推導翻譯正確的模版代碼。

class IF{};
class A:public IF
{
public:
    void Speak() {}
};
class B
{
};

// 1
template<typename T>
bool Check(typename std::enable_if<std::is_base_of<IF, T>::value>::type *)
{
    cout << "這是個能說會道的";
    return true;
}
// 2
template<typename T>
bool Check(typename std::enable_if<!std::is_base_of<IF, T>::value>::type *)
{
    cout << "這個大概不會說話吧";
    return false;
}

cout << "A:" << Check<A>(new A()); // 匹配1
cout << "B:" << Check<B>(new B()); // 匹配2

enable_if_t

enable_if_t是C++ 14支持的,是對enable_if的一個擴展,其實現一般爲:

template< bool B, class T = void >
using enable_if_t = typename enable_if<B,T>::type;

因此enable_if_tenable_if<B,T>::type的一個別稱,接收一個布爾值和返回類型的參數,上述的代碼可改爲:

// 3
template<typename T>
bool Check(std::enable_if_t<std::is_base_of<IF, T>::value> *)
{
    cout << "這是個能說會道的";
    return true;
}
// 4
template<typename T>
bool Check(std::enable_if_t<!std::is_base_of<IF, T>::value> *)
{
    cout << "這個大概不會說話吧";
    return false;
}
cout << "A:" << Check<A>(new A()); // 匹配3
cout << "B:" << Check<B>(new B()); // 匹配4

匿名函數

匿名函數也就是常說的lambda表達式,現在大部分的高級語言都已經支持了,C++11也開始支持這一特性,其語法形式如下:

// [capture list] (params list) mutable exception-> return type { function body };
// [捕獲列表] (形參列表) 是否可以修改捕獲的變量 異常設定-> 返回值 { 函數體 };

匿名函數大致實現原理:編譯器通過爲每一個lambda表達式生成一個閉包類實現,捕獲的對象作爲類成員數據,定義一個operator()重載,執行體就是lambda中的執行體,並且用const修飾,也就是你不能修改成員數據。如果對象設置的捕獲方式指定爲傳值,成員對象則聲明爲被捕獲對象的類型並不做修飾,如果指定爲引用,則成員對象聲明爲被捕獲對象類型的指針並用mutable修飾,這樣你就能在const修飾的執行體中修改引用的對象。

被聲明定義的匿名函數可直接調用,也可以賦值給變量後調用,因此匿名函數有點像函數指針。

常用的使用方法如下:

// [capture list] (params list) mutable exception-> return type { function body };
// [捕獲列表] (形參列表) 是否可以修改捕獲的變量 異常設定-> 返回值 { 函數體 };
const int ca = 1024;
int vb = 0;
vector<int> va;

// void () 各種不同的寫法,無形參時不指定參數列表,
// 返回值可省略,會根據return自動補充返回值類型
[] { show("Lambda1"); };                // 不會被調用,僅作爲定義
[] { show("Lambda2"); }();              // 可以被調用,定義調用執行
[]() { show("Lambda3"); }();
[]() -> void { show("Lambda4"); }();
[]() -> void {show("Lambda5"); }();
int ret = []() -> int { return 1; }();
ret = []() { return 1; }();              // 自動補充返回值類型:int
[]() { return; }();                      // 無返回值類型

// []{ show(ca); }; -> 編譯出錯,未設置捕獲模式
[ca] { show(ca); }();
// [vb]() { vb += 1; }; -> // 編譯出錯,捕獲變量爲傳值類型時,Lambda函數內默認不可修改外部變量
[vb]() mutable { vb += 1; }();   // 捕獲變量爲傳值類型時,與函數傳值類型參數相似
[&vb]() mutable { vb += 1; }();  // 捕獲變量爲引用類型時,與函數引用類型參數相似
[&vb]() { vb += 1; }();          // 捕獲變量爲引用類型時,默認爲mutable

// 匿名函數賦值
auto mydouble = [](int v) { return v + v; };
mydouble(vb);
mydouble(ca);

// 排序
vector<int> vecAge = { 1, 2, 9, 6, 5 };
std::sort(vecAge.begin(), vecAge.end(), [](const int& a, const int& b) { return a < b; });

auto mycmp = [](const int& a, const int& b) { return a > b; };
std::sort(vecAge.begin(), vecAge.end(), mycmp);

強類型枚舉 enum class

enum class LogFlag
{
    NONE = 0,
    ERROR = 1,
    WARN = 2,
    INFO = 3,
    DEBUG = 4
};

enum class RecvFlag
{
    NONE = 0,
    RECVING = 1,
    ERROR = 2,
    FINISHED = 3
};

int main()
{
    cout << static_cast<int>(LogFlag::NONE) << endl;
    cout << static_cast<int>(LogFlag::ERROR) << endl;
    cout << static_cast<int>(RecvFlag::NONE) << endl;
    cout << static_cast<int>(RecvFlag::ERROR) << endl;
    return 0;
}

通過enum class就允許枚舉名字名字相同,解決枚舉名字衝突問題。此外,該種枚舉爲特定的枚舉類型,每個定義都是一種新類型,因此可以強區分類型,該枚舉爲類型安全的,枚舉類型不能隱式地轉換爲整數,故不能直接與整數數值做比較。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章