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

《Modern C++ Design》之上篇的更多相关文章

  1. 读-《c++设计新思维-泛型编程与设计模式之应用》经典记录(英文书名:《modern c++ design》)

    1.以设计为目标的程序库都必须帮助使用者完毕静止的设计.以实现使用者自己的constraints,而不是实现预先定义好的constraints. 2.Anything that can be done ...

  2. C++程序设计之四书五经[转自2004程序员杂志]--上篇

    C++程序设计之四书五经 作者:荣耀 C++是一门广泛用于工业软件研发的大型语言.它自身的复杂性和解决现实问题的能力,使其极具学术研究价值和工业价值.和C语言一样,C++已经在许多重要的领域大获成功. ...

  3. Architecture and design 洋葱 中间件 装饰器

    Go kit - Frequently asked questions https://gokit.io/faq/ Architecture and design Introduction - Und ...

  4. 推荐书目 - C++学习资料

    前言 在本文的前半部分我我会谈谈 我看过的书,和我个人的一些理解 ,并且会提供 C++标准委员会相关链接 和 C++第三方轮子/库总结 .本文的后半部分翻译了来自 The Definitive C++ ...

  5. [转载]锁无关的数据结构与Hazard指针——操纵有限的资源

    Lock-Free Data Structures with Hazard Pointers 锁无关的数据结构与Hazard指针----操纵有限的资源 By Andrei Alexandrescu a ...

  6. BFS/DFS算法介绍与实现(转)

    广度优先搜索(Breadth-First-Search)和深度优先搜索(Deep-First-Search)是搜索策略中最经常用到的两种方法,特别常用于图的搜索.其中有很多的算法都用到了这两种思想,比 ...

  7. 内存管理内幕mallco及free函数实现

    原文:https://www.ibm.com/developerworks/cn/linux/l-memory/ 为什么必须管理内存 内存管理是计算机编程最为基本的领域之一.在很多脚本语言中,您不必担 ...

  8. 转 一个典型的 C++ 程序员成长经历:

    1.  完整的学一遍 C++ 所有语言特性,典型书籍 "The C++ Programming Language" Part1, Part2, "C++ Primer&q ...

  9. 实现一个 Variant

    很多时候我们希望能够用一个变量来保存和操作不同类型的数据(比如解析文本创建 AST 时保存不同类型的结点),这种需求可以通过继承来满足,但继承意味着得使用指针或引用,除了麻烦和可能引起的效率问题,该做 ...

  10. c++ 模板元编程的一点体会

    趁着国庆长假快速翻了一遍传说中的.大名鼎鼎的 modern c++ design,钛合金狗眼顿时不保,已深深被其中各种模板奇技淫巧伤了身...论语言方面的深度,我看过的 c++ 书里大概只有 insi ...

随机推荐

  1. #二分图匹配#洛谷 5771 [JSOI2016]反质数序列

    题目 给出 \(n\) 个正整数,问最大的子集使得任意两个数的和都不是质数 \(n\leq 3*10^3\) 分析 如果把两个数的和为质数连边,等价于求最大独立集. 由于只有偶数加奇数才可能产生质数( ...

  2. 开放原子开源基金会OpenHarmony工作委员会主席侯培新寄语OpenAtom OpenHarmony分论坛

    2022开放原子全球开源峰会 OpenAtom OpenHarmony分论坛 万物互联,使能千行百业 7月27日 14:00  与您相约 OpenHarmony 工作委员会主席侯培新 寄语 OpenA ...

  3. API 参考与帮助内容:一站式开发与使用者支援

    API 文档 API 文档是旨在了解 API 详细信息的综合指南.通常,它们包括端点.请求示例.响应类别和示例以及错误代码等信息.API 文档可帮助开发人员了解 API 端点的具体细节,并了解如何将 ...

  4. 一个库帮你快速实现EF Core数据仓储模式

    前言 EF Core是我们.NET日常开发中比较常用的ORM框架,今天大姚要分享的内容是如何使用EF Core Generic Repository通用仓储库来快速实现EF Core数据仓储模式. E ...

  5. 学习笔记——Python基础

    字符串索引 str = '我是一名学生' print(str[0]) #输出"我" print(str[-6]) #输出"我" 字符串切片:把数据对象的一部分拿 ...

  6. Linux忘记root密码修改方法

    1. 再开机启动的时候按键盘上的"e"键会出现如下界面.再次按"e"键进行选择相应的内核. 2. 选择相应的内核,再次按"e". 3.经过第 ...

  7. centos环境Jenkins配置war包Tomcat

    末尾有软件安装包,自行下载(centos环境) centos JDK Jenkins maven tomcat git myslq nginx 7.9 11.0.19 2.418 3.8.1 9.0. ...

  8. 基于Traefik如何实现向后转发自动去掉前缀?

    前言 Traefik 是一个现代的 HTTP 反向代理和负载均衡器,使部署微服务变得容易. Traefik 可以与现有的多种基础设施组件(Docker.Swarm 模式.Kubernetes.Mara ...

  9. 【鸿蒙千帆起】高德地图携手HarmonyOS NEXT,开启智能出行新篇章

    2024年1月18日下午,华为举办了鸿蒙生态千帆启航仪式,对外宣布HarmonyOS NEXT星河预览版现已开放申请,同时,首批200+鸿蒙原生应用加速开发,鸿蒙生态设备数量更是突破了8亿大关.这些进 ...

  10. 开发案例:使用canvas实现图表系列之折线图

      一.功能结构 实现一个公共组件的时候,首先分析一下大概的实现结构以及开发思路,方便我们少走弯路,也可以使组件更加容易拓展,维护性更强.然后我会把功能逐个拆开来讲,这样大家才能学习到更详细的内容.下 ...