《Modern C++ Design》之上篇

发布时间 2023-08-04 17:44:21作者: Aurelius84

如下内容是在看侯捷老师翻译的《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