如下内容是在看侯捷老师翻译的《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. KingbaseES V8R3数据库运维案例之---不完整的启动包(incomplete startup packet)复现

    案例说明: 在KingbaseES V8R3数据库的sys_log日志中,出现以下故障信息"不完整的启动包(incomplete startup packet)"日志信息.本案例复 ...

  2. KingbaseES V8R6 sslinfo 插件

    前言 KingbaseES对使用SSL 连接加密客户端/服务器通讯的本地支持,可以增加数据传输安全性. 本文展示配置ssl连接,并通过安装一个插件验证ssl加密认证使用. 一.配置ssl连接过程: s ...

  3. .Net Core AutoFac 使用方法讲解大全,具体详细使用知识总结

    AutoFac 具体使用知识总结 阅读前提示 AutoFac 只是众多IOC框架的其中一种, 比较主流的有Unity.autofac.spring.net.MEF.Injection.Asp.Net ...

  4. 6 CSS样式继承

    6 样式继承 CSS的样式表继承指的是,特定的CSS属性向下传递到子孙元素.总的来说,一个HTML文档就是一个家族,然后html元素有两个子元素,相当于它的儿子,分别是head和body,然后body ...

  5. #Tarjan#洛谷 4819 [中山市选]杀人游戏

    题目 分析 缩点后显然只考虑入度为0的点的个数, 但是问题是如果有一个入度为0的点缩点前只有1个点 且它的出边上的所有点都可以被其它入度为0的点遍历, 那么可以将其它点全部排除后剩下的这个点就是凶手, ...

  6. #树状数组,概率,离散,双指针#洛谷 6834 [Cnoi2020]梦原

    题目 分析 如果是序列(\(k=1\))也就是积木大赛 那也就是\(\sum_{i=1}^n\max\{a_i-a_{i-1},0\}\) 那关键就是要处理与父节点之间的关系,如果父节点的值小于该节点 ...

  7. OpenHarmony——内核IPC机制数据结构解析

    一.前言 OpenAtom OpenHarmony(以下简称"OpenHarmony")是由开放原子开源基金会(OpenAtom Foundation)孵化及运营的开源项目,目标是 ...

  8. SQL 数据库语句- 创建和管理数据库

    SQL CREATE DATABASE 语句 SQL CREATE DATABASE 语句用于创建一个新的 SQL 数据库. 语法 CREATE DATABASE 数据库名称; 示例 以下 SQL 语 ...

  9. Go 语言变量类型和声明详解

    在Go中,有不同的变量类型,例如: int 存储整数(整数),例如123或-123 float32 存储浮点数字,带小数,例如19.99或-19.99 string - 存储文本,例如" H ...

  10. Matplotlib绘图设置---坐标轴刻度和标签设置

    每个axes对象都有xaxis和yaxis属性,且xaxis和yaxis的每一个坐标轴都有主要刻度线/标签和次要刻度线/标签组成,标签位置通过一个Locator对象设置,标签格式通过一个Formatt ...