《Modern C++ Design》之上篇

如下內容是在看侯捷老師翻譯的《Modern C++ Design》書籍時,整理的code和摘要,用於不斷地溫故知新。

第一章

1. 運用 Template Template 參數實作 Policy Classes

template <template <class Created> class CreationPolicy> 
// template <template <class> class CreationPolicy>  <---- 也可以這樣寫
class WidgetManager : public CreationPolicy<Widget>
{...};

// 使用端
WidgetManager<OpNewCreator> MyWidgetMgr; // <--- 並未提供 Widget 模版參數

CreatedCreationPolicy 的參數,CreationPolicy 則是 WidgetManager 的參數。 Widget 已經顯式地在 public 後寫出了,所以使用時不需要再傳一次參數給 Policy
儘管在模版裏寫出了 Created ,但並沒有使用到,也沒有啥貢獻,只是 CreationPolicy 的形式引數(formal argument)

從易用性角度而言,我們可以提供一些常用的 policies ,並且以“template 缺省參數”的形式提供:

template <template <class> class CreationPolicy = OpNewCreator>
class WidgetManager : ....

注意:policies 與虛函數有很大不同。policies 因爲有豐富的型別信息及靜態鏈接等特性,所以是建立「設計元素」時的本質性東西。即「設計」指定了「執行前型別如何互相作用、你能夠做什麼、不能夠做什麼」的完整規則。此外,由於編譯期纔將 host class 和其 policies 結合在一起,因此更加牢固和高效。

缺點:由於 policies 特質,不適用於動態鏈接和二進位接口。作者認爲如下的方式「難以討論、定義、實作和運用」

struct OpNewCreator {

  template <class T>
  static T* Create(){
    return new T;
  }
};

2. Poilic Class 的析構函數

許多 Policies 並無任務數據成員、純粹只是規範行爲,若給基類加入一個虛函數,會額外增加對象大小(引入一份 vptr )。一種解法是:採用 protected 繼承或者 private 繼承(但會失去很多豐富的特性)。更輕便和有效率的解法是:定義一個 non-virtual protected 析構函數:

struct OpNewCreator {

  template <class T>
  static T* Create(){
    return new T;
  }
 // 只有派生類得到的Class 纔可以摧毀這個policy對象。避免了外界通過delete 指向基類的指針的用法。
 protected:
     ~OpNewCreator(){} // 非虛函數,無大小和速度上的開銷
};

3. 通過不完全具現化而獲得的選擇性機能

如果 class template 有一個成員函數未曾被用到,他就不會被編譯器具體實現出來,編譯器不會理他,甚至不會爲他進行語法檢查。

4. 結合 Policy Classes

當你將 policies 組合起來時,便是它們最有用的時候。

template<
    class T,
    template <class> class CheckingPolicy,
    template <class> class ThreadingModel
>
class SmartPtr  // <--- 「集成數個 policies」 的協調層
    : public CheckingPolicy<T>
    , public ThreadingModel<SmartPtr>{
  
    ....
    T* operator->(){
        typename ThreadingModel<SmartPtr>::Lock guard(*this);
        CheckingPolicy<T>::Check(pointee_);
        return pointee_;
    }  
  private:
      T* pointee_;

};

// 使用端
typedef SmartPtr<Widget, NoChecking, SingleThreaded> WidgetPrt;

上述同一函數中對 checkingPolicyThreadingModel 的兩個 policy classes 的運用。根據不同的 template 參數,SmartPtr::operator-> 會表現出兩種不同的正交行爲,這正是 policies 的組合威力所在。

5. 以 Policy Classes 定製結構

雖然 templates 具有「無法定製 class 的結構,只能定製其行爲」的限制,但 policy-based design 支持結構方面的定製。

template <class T>
class DefaultSPStorage
{
public:
    typedef T* PointerType;
    typedef T& ReferenceType;
protected:
    PointerType GetPointer() {return ptr_;}
    void SetPointer(PointerType ptr){ ptr_ = ptr;}
 private:
     PointerType ptr_;
};

tempalte
<
    class T,
    template <class> class CheckingPolicy,
    template <class> class ThredingModel,
    template <class> class Storage = DefaultSPStorage  // <——- 可實現指針類型的屏蔽
 >
 calss SmartPtr;

6. Policies 的兼容性

Policies 之間彼此轉換的各種方法中,最好又最具擴充性的方法是「以 Policy 控制 SmartPtr 對象的拷貝和初始化」,如下例子:

template<class T, template <class> class CheckingPolicy>
class SmartPtr : public CheckingPolicy<T>{
    ...
    template<class T1, template <class> class CP1>
    SmartPtr(const SmartPtr<T1, CP1>& other)
        : pointee_(other.pointee_), CheckingPolicy<T>(other)
        {...}
};
  • 假設 ExetendWidget 派生自 Widget。當以 SmartPtr<ExtendWidget, NoChecking> 初始化一個 SmartPtr<Widget, NoChecking> 時,編輯器會嘗試以一個 ExtendWidget* 初始化 Widget*(這會成功),然後以一個 SmartPtr<Widget, NoChecking> 初始化 NoChecking。前者是派生自後者的,所以編譯器是很容易知道你想做什麼,也會正確幫你這麼做。

  • 當以 SmartPtr<ExtendWidget, NoChecking> 初始化一個 SmartPtr<Widget, EnforceNotNull> 時,編譯器就會嘗試將 SmartPtr<ExtendWidget, NoChecking> 拿來匹配 EnforceNotNull 構造函數。則依賴於EnforceNotNull 是否有對應的夠咱函數,若有,則轉換成功。或者 NoChecking 有對應的轉型操作符,則也會轉換成功。除此之外,都會編譯錯誤。
    這裏有一個典型的相關case:std::autop_ptr(C++11已不推薦使用了)。

7. 將一個 Class 分解爲一堆 Policies

建議 Policy-based class design 的最困難的部分,便是如何將 class 正確地分解爲 policies。一個準則就是「將參與 class 行爲的設計鑑別出來,並命名之」。任何處理邏輯只要有「一種以上的方法解決」,都應該被分析出來,並獨立爲 Policy。但「過度泛化」的 host classes 會產生缺點,會有過多的 template 參數。

Policy 之間的邊界怎麼確定呢?保持正交分解很重要。不正交的分解——如果各式各樣的 policies 需要知道彼此。

template <class T>
struct IsArray{
 T& ElementAt(T* ptr, size_t idx) {return ptr[idx];}
 ....
};

template <class> T
struct IsNotArray {};

假設還有另一個 Policy 負責析構。此時無論 SmartPtr 是否指向 Array,都會與析構的 Policy 耦合,因爲析構的 PolicyIsArray 下使用 delete [],在 IsNotArray 下使用 delete。因此 ArrayDestroy 不是正交的。非正交的 policies 是不完美的設計,應該儘量避免,會給 host classpolicy class 引入額外的複雜度。

8. 總結

「設計」就是一種「選擇」,大多數時候我們的困難並不在於找不到解決方案,而是有太多方案。Policies 機制由 templates 和 多重繼承組成,Host class 的所有機能都來自 policies,運作起來就像一個聚合無數個 Policies 的容器。

第二章

1. 編譯期 Assertions

表達式在編譯期評估所得的結果是個定值(常數),這意味着你可以用利用編譯器來做檢查。最簡單的方式稱爲 compile-time assertions,在C和C++語言中都可以實現,它依賴一個事實:大小爲 0 的 array 是非法的。

#define STATIC_CHECK(expr) { char unnamed[(expr) ? 1: 0];}  // <---- 最初版本

template <class To, class From>
To Safe_reinterpret_cast(From from)
{
    STATIC_CHECK(sizeof(from) <= sizeof(To));
    return reinterpret_cast<To>(from);
}

但上述實現無法提供「可讀、友好、可定製」的報錯信息,較好的解法是依賴一個名稱帶有意義的 template

template <bool> struct CompiledTimeError;
template <> struct CompiledTimeError<true>{};  // <--- 僅支持對 true 進行具現化

#define STATIC_CHECK(expr) (CompiledTimeError<(expr) != 0>())

爲了更進一步支「可定製化」的報錯信息,我們可以進階地修改爲:

template<bool> 
struct CompiledTimeChecker{
    CompiledTimeChecker(...); // <--- C++ 支持的非定量任意參數
}

template<> struct CompiledTimeChecker<false>{};  // <-- 僅對 false 進行具現化

#define STATIC_CHECK(expr, msg)    \
{                                  \
  class ERROR_##msg {};            \    // <--- local 空類
  (void)sizeof(CompiledTimeChecker<(expr)>(Error_##msg)); \  // <--- Error_##msg 是類的初始化參數,sizeof最終會被調用
}

當表達式爲 false 時,編譯器找不到將 Error_##msg 轉成 CompiledTimeChecker<false> 的方法,而且會報出:Error: Cannot convert Error_xxx to CompiledTimeChecker<false>

2. 模版偏特化

通常在一個 class template 偏特化定義中,你只會特化某些 template 參數,而留下其他泛化參數,編譯器會嘗試找出「最匹配」的定義,雖然這個過程十分複雜和精細。

template <class Window, class Controller>
class Widget {....};

template <class ButtonArg>   // <---- 支持富有創意的偏特化
class Widget<Button<ButtonArg>, MyController> {...};

但偏特化機制不能作用在「函數」身上,不論是成員函數還是非成員函數

  • 可以「全特化」class template 中的成員函數,但不能「偏特化」他們
  • 不能偏特化 namespace-level(non-member) 函數,但可以藉助函數重載實現類似的效果。
template <class T, Class U>
T Func(U obj);

template <class U>
void Func<void, U>(U obj);  // <---- 非法

template <class T>
T Func(Window obj);       // <---- 合法,overloading 機制

3. 局部類 Local Classes

C++ 支持在函數中定義 class,是的,沒有看錯,是在函數中定義,但有一些侷限性:

  • local class 不能定義 static 成員變量,也不能訪問 non-static 局部變量

有趣的是,local class 可以使用函數的 template 參數。當然,任何運用 local class 的手法,都可以改用「函數外的 template class」 來完成。但 local class 可以簡化操作並提高「符號地域性」

class Interface {
public:
  virtual void Fun() = 0;
};

template <class T, class P>
Interface* MakeAdapter(const T& obj, const P& arg){

    class Local : public Interface {    // <--- 內部類
      public:
          Local(const T& obj, const P& arg): obj_(obj), arg_(arg) {}
          virtual void Fun() {obj_.Call(arg_);}
       private:
         T obj_;
         P arg_;
    };
    
    return new Local(obj, arg);
}

local class 還有一個隱藏特性:它有 final 的語義。即外界不能繼承一個隱藏於函數內的 class

4. 常整數映射爲型別

如下是作者提出的一個思路,比較有意思,藉由「不同的 template 具現體本身就是不同的類型」。

template <int v>
struct Int2Type{
  enum {value = v};
};

上述用於產生類別的數值是一個「枚舉值」,可根據編譯期計算出來的結果選用不同的函數,達到「運用常數來靜態分派」的功能。那在什麼場景下會用到這個手法呢?

  • 有必要根據某個編譯期常數調用一個或不同的函數
  • 有必要在編譯期實施「分派」(dispatch

相對而言,執行期分派有時並非如我們預期,在編譯器層面可能會報錯,如下例子:

template <typename T, bool isPoly>
class NiftyContainer{
  void DoSomething(){
      T* pSomeObj = ...;
      if(isPoly){     // <--- 運行時分派
        T* pNewObj = pSomeObj->Clone();   // <--- 位置①
        .... (多態算法)
      }else{
        T* pNewObj = new T(*pSomeObj);  // copy 構造, 位置②
        ....(非多態算法)
      }
  }
};

如果你調用 NiftyContainer<int, false>DoSomething() ,當模版參數 T 類別沒有定義成員函數 Clone() 時,上述代碼會在位置①編譯報錯。因爲編譯器總是勤奮地編譯所有的分支。

Int2Type 提供了一種明確的解法,其奧義在於「編譯器並不會去編譯一個未被使用到的 template 函數,只會做文法檢查而已」。

....
{
public:
    void DoSomething(T* pObj){ DoSomething(pObj, Int2Type<isPoly>);}

private:
    void DoSomething(T* pObj, Int2Type<true>){
        T* pNewObj = pObj->Clone();
        .... (多態算法)
    }
 
    void DoSomething(T* pObj, Int2Type<false>){
        T* pNewObj = new T(*pObj);
        ....(非多態算法)
    }  
};

5. 型別對型別的映射

template 函數不支持偏特化,我們有辦法模擬實現類似的機制麼?假設我們要針對 Widget 的創建過程偏特化,因爲它的構造函數有兩個參數。

template <class T, class U>
T* Create(const U& arg){
    return new T(arg);
}

// 初版方案:藉助重載機制
template <class T, class U>
T* Create(const U& arg, T /*dummy*/){
    return new T(arg);
}

template <class U>
Widget* Create(const U& arg, Widget /*dummy*/){
    return new Widget(arg, -1);
}

上述方案會構造未使用的對象,造成額外開銷。此處我們引入 Type2Type

template <class T>
struct Type2Type{
    typedef T OriginalType;  // <---- 沒有任何數值
};

template <class T, class U>
T* Create(const U& arg, Type2Type<T>){
    return new T(arg);
}

template <class U>
Widget* Create(const U& arg, Type2Type<Widget>){
    return new Widget(arg, -1);
}

// 使用端
String* pStr = Create("hello", Type2Type<String>())();
Widget* pW = Create(100, Type2Type<Widget>())();

Type2Type 參數只是用來選擇合適的「重載函數」。

6.型別選擇

在前面的 NiftyContainer 例子中,你可能會選擇 std::vector 作爲後端的存儲結構,對於多態類型,不能存儲實例,必須存儲指針;對於非多態類型,可以存儲實例(這樣效率更高)。你可能會想到根據 isPoly 參數動態決定將 ValueType 定義爲 T*T,如下:

template <class T, bool isPoly>
struct NiftyContainerValueTraits {
    typedef T* valueType;
};

template <class T>
struct NiftyContainerValueTraits<T, false> {
    typedef T valueType;
};

template <class T, bool isPoly>
class NiftyContainer{
    ...
    typedef NiftyContainerValueTraits<T, isPoly> Traits;
    typedef typename Traits::ValueType ValueType;   // <---- 藉助 Traits 機制
};

如上實現方案,針對不同的類,都必須定義專屬的 Traits class template。(爲什麼?不是隻針對「是否多態」進行偏特化就可以了,爲什麼這裏會說對不同的類也要定義專屬的 Traits 呢?)
Loki 裏的實現是如下機制:

template <bool flag, class T, class U>
struct Select{
   typedef T Result;
};

template <class T, class U>
struct Select<false, T, U>{
    typedef U Result;
};

template <class T, bool isPoly>
class NiftyContainer{
    ...
    typedef Select<isPoly, T*, T>::Result ValueType;
};

7. 編譯期間偵測可轉換性和繼承性

對於兩個陌生的類型 TU ,如何知道 U 是否繼承自 T ? 可以合併運用 sizeof 和重載函數,如下是魔法產生的樣例代碼:

template<class T, class U>
class Conversion{
  typedef char Small;
  class Big {char dummy[2];};
  static Small Test(U);
  static Big Test(...);
  static T MakeT(); // not implemented
  
public:
  enum {exists = sizeof(Test(MakeT())) == sizof(Small);}
  enum {sameType = false;}
};

template <class T>   // 偏特化
class Conversion<T, T>{
public:
   enum {exists = 1, sameType = 1};
};

// 用戶端代碼
int main(){
  using namespace std;
  cout << Conversion<double, int>::exists << endl;  // 1
  cout << Conversion<char, char*>::exists << endl;  // 0
  cout << Conversion<size_t, vector<int>>::exists << endl;  // 0
}

有了 Conversion 的幫助,我們很容易在編譯期判斷兩個 class 是否具有繼承關係:

#define SUPER_SUB_CLASS(T, U) \
    (Conversion<const U*, const T*>::exists &&  \
    !Conversion<const T*, conost void*>::sameType)

如果 Upublic 繼承自 T ,或 TU 是同一類別,SUPER_SUB_CLASS(T, U) 會返回 true。爲什麼這些代碼要加上 const 修飾?原因是我們不希望因爲 const 而導致轉型失敗。

8. type_info 的一個 Wrapper

type_info 常常和 typeid 操作符一起使用,後者返回一個 reference,指向一個 type_info 對象:

void func(Base* ptr){
if(typeid(*ptr) == typeid(Derived)){
   //.....
 }
}

typd_info 支持 operator==operator!=,還提供了額外的兩個函數:

  • name(),返回一個 const char*
  • before(),帶來 type_info 對象的次序關係,可以藉助此接口對 type_info 對象建立索引
    type_info 關閉了 copy 構造函數和賦值構造函數,導致不可以存儲它,但可以存儲它的指針,因爲 typeid 傳回的對象採用的是 static 存儲方式,不用擔心生命週期問題。但C++並不保證每次調用 typeid(int) 會傳回“指向同一個 type_info 對象”的 reference
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章