《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 模版参数
Created 是 CreationPolicy 的参数,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;
上述同一函数中对 checkingPolicy 和 ThreadingModel 的两个 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 耦合,因为析构的 Policy 在 IsArray 下使用 delete [],在 IsNotArray 下使用 delete。因此 Array 与 Destroy 不是正交的。非正交的 policies 是不完美的设计,应该尽量避免,会给 host class 和 policy 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. 编译期间侦测可转换性和继承性
对于两个陌生的类型 T 和 U ,如何知道 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)
如果 U 是 public 继承自 T ,或 T 和 U 是同一类别,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》之上篇的更多相关文章
- 读-《c++设计新思维-泛型编程与设计模式之应用》经典记录(英文书名:《modern c++ design》)
1.以设计为目标的程序库都必须帮助使用者完毕静止的设计.以实现使用者自己的constraints,而不是实现预先定义好的constraints. 2.Anything that can be done ...
- C++程序设计之四书五经[转自2004程序员杂志]--上篇
C++程序设计之四书五经 作者:荣耀 C++是一门广泛用于工业软件研发的大型语言.它自身的复杂性和解决现实问题的能力,使其极具学术研究价值和工业价值.和C语言一样,C++已经在许多重要的领域大获成功. ...
- Architecture and design 洋葱 中间件 装饰器
Go kit - Frequently asked questions https://gokit.io/faq/ Architecture and design Introduction - Und ...
- 推荐书目 - C++学习资料
前言 在本文的前半部分我我会谈谈 我看过的书,和我个人的一些理解 ,并且会提供 C++标准委员会相关链接 和 C++第三方轮子/库总结 .本文的后半部分翻译了来自 The Definitive C++ ...
- [转载]锁无关的数据结构与Hazard指针——操纵有限的资源
Lock-Free Data Structures with Hazard Pointers 锁无关的数据结构与Hazard指针----操纵有限的资源 By Andrei Alexandrescu a ...
- BFS/DFS算法介绍与实现(转)
广度优先搜索(Breadth-First-Search)和深度优先搜索(Deep-First-Search)是搜索策略中最经常用到的两种方法,特别常用于图的搜索.其中有很多的算法都用到了这两种思想,比 ...
- 内存管理内幕mallco及free函数实现
原文:https://www.ibm.com/developerworks/cn/linux/l-memory/ 为什么必须管理内存 内存管理是计算机编程最为基本的领域之一.在很多脚本语言中,您不必担 ...
- 转 一个典型的 C++ 程序员成长经历:
1. 完整的学一遍 C++ 所有语言特性,典型书籍 "The C++ Programming Language" Part1, Part2, "C++ Primer&q ...
- 实现一个 Variant
很多时候我们希望能够用一个变量来保存和操作不同类型的数据(比如解析文本创建 AST 时保存不同类型的结点),这种需求可以通过继承来满足,但继承意味着得使用指针或引用,除了麻烦和可能引起的效率问题,该做 ...
- c++ 模板元编程的一点体会
趁着国庆长假快速翻了一遍传说中的.大名鼎鼎的 modern c++ design,钛合金狗眼顿时不保,已深深被其中各种模板奇技淫巧伤了身...论语言方面的深度,我看过的 c++ 书里大概只有 insi ...
随机推荐
- JDBC复习:创建MySQL数据表
1 try { 2 conn=JDBCUtil.getConnection(); 3 preparedStatement = conn.prepareStatement(DROP_TABLE_1); ...
- 强烈推荐:2024 年12款 Visual Studio 亲测、好用、优秀的工具,AI插件等
工具类扩展 1. ILSpy 2022 (免费) ILSpy 是 ILSpy 开源反编译器的 Visual Studio 扩展. 是一款开源.免费的.且适用于.NET平台反编译[C#语言编写的程序和库 ...
- [网络/HTTPS/Java] PKI公钥基础设施体系、CA证书与认证工具(jre keytool / openssl)
0 序 1 CA证书概述 说起 HTTP 的那些事,则不得不提 HTTPS ,而说起 HTTPS ,则不得不提数字证书. 本文将从 Java 的角度,学习 HTTPS 和数字证书技术. 1.1 访问 ...
- 鸿蒙手表定位功能Demo体验,适用儿童、老年和外出旅游安全市场
针对儿童和老人,可穿戴的智能手表用处很大.市场也有许多类似的产品,支持接打电话.支付扫码.定位等功能,属于新兴的商业机会.依托华为品牌,鸿蒙手表也致力为用户打造精品的.产品质量佳.可穿戴的智能体验.对 ...
- 第二十一篇:信号、缓存、中间件、Form操作
一.CSRF 二.中间件 三.缓存 四.信号 五.Form操作
- UML 哲学之道——启航篇[一]
前言 简单去介绍一下uml的哲学之道也是自我整理之道. 正文 什么是uml,全程是统一建模语言(unified modeling language),简单的说就是用图形来表示文档. 是描述构造和文档化 ...
- CSS 样式清单整理(二)
16.元素占满整个屏幕 heigth如果使用100%,会根据父级的高度来决定,所以使用100vh单位. .dom{ width:100%; height:100vh; } 17.CSS实现文本两端对齐 ...
- CentOS7.9 systemctl
目录 命令格式 语法 加载配置文件 关机和开机 unit 文件存放位置 unit 格式说明 service unit file 文件构成部分 unit 段的常用选项 service 段的常用选项 in ...
- Linux下的常见基本指令
pwd //显示当前用户所在的路径 ls //显示当前路径下的文件名或者目录名称 ls-l //显示当前路径下的文件或者目录的更详细的属性信息 cd 一个目录路径 //进入一个目录,进去后,可以用pw ...
- 容器启动流程(containerd 和 runc)
启动流程 containerd 作为一个 api 服务,提供了一系列的接口供外部调用,比如创建容器.删除容器.创建镜像.删除镜像等等.使用 docker 和 ctr 等工具,都是通过调用 contai ...