C++设计模式(一)基于 Policy 的 class 设计
原图
概述
这一章将介绍所谓 policies 和 policy classes 它们是一种重要的 classes 设计技术,能够增加程序库的弹性并提高复用性,这正是 Loki 的目标所在。简言之,具备复杂功能的 policy based class 由许多小型 classes(称为 policies)组成。每一个这样的小型 class 都只负责单纯如行为或结构的某一方面( behavioral or structural aspect)。一如名称所示,一个 policy 会针对特定主题建立个接口。在“遵循 policy 接口”的前提下,你可以用任何适当的方法来实作 policies。
由于你可以混合并匹配各种 policies,所以藉由小量核心基础组件( core elementary components)的组合,你可完成一个“行为集”( behaviors set)。
软件设计的多样性(Multiplicity)
让我们考虑一个简単的入门级程序维型:-个 Smart Pointer(智能指针)。这种 class 可被用于单线程或多线程之中,可以运用不同的 ownershi ...
C++11(六)nullptr 与 Lambda
原图
nullptr
nullptr、NULL 和 0
般情况下,NULL 是一个宏定义。在传统的 C 头文件( stddef.h)里我们可以找到如下代码
123456#undef NULL#if defined(__cplusplus)#define NULL O#else#define NULL ((void *)0)#endif
可以看到,NULL 可能被定义为字面常量 0,或者是定义为无类型指针(void*)常量。不过无论采用什么样的定义,我们在使用空值的指针时,都不可避免地会遇到一些麻烦。
12345678910111213141516171819void func(int i){ std::cout << "int" << std::endl;}void func(void* p){ std::cout << "ptr" << std::endl;}int main(){ func(0); // int func(NULL); / ...
C++11(五)constexpr、原子与线程存储
原图
常量表达式(constexpr)
常量表达式机制是为了:
提供一种更加通用的常量表达式。
允许用户自定义的类型成为常量表达式。
提供了一种保证在编译期完成初始化的方法(可以在编译时期执行某些函数调用)。
考虑下面这段代码:
1234567891011121314enum Flags { good=0, fail=1, bad=2, eof=4 };constexpr int operator|(Flags f1, Flags f2){ return Flags(int(f1)|int(f2)); }void f(Flags x){ switch (x) { case bad: /* … */ break; case eof: /* … */ break; case bad|eof: /* … */ break; default: /* … */ break; }}
虽然“bad|eof”是一个表达式,但是因为这两个参数都是常量,在编译时期,就可以计算出它的结果,因而可以作为常量 ...
C++11(四)智能指针
原图
shared_ptr
实现原理
一个 shared_ptr 对象的内存开销要比裸指针和无自定义 deleter 的 unique_ptr 对象略大。shared_ptr 需要维护的信息有两部分:
指向共享资源的指针
引用计数等共享资源的控制信息——实现上是维护一个指向控制信息的指针
所以,shared_ptr 对象需要保存两个指针。shared_ptr 的 deleter 是保存在控制信息中,所以,是否有自定义 deleter 不影响 shared_ptr 对象的大小。当我们创建一个 shared_ptr 时,其实现一般如下:
1std::shared_ptr<T> sptr1(new T);
复制一个 shared_ptr :
1std::shared_ptr<T> sptr2 = sptr1;
为什么控制信息和每个 shared_ptr 对象都需要保存指向共享资源的指针?可不可以去掉 shared_ptr 对象中指向共享资源的指针,以节省内存开销?
答案是:不能。 因为 shared_ptr 对象中的指针指向的对象不一定和控制块中的指针 ...
C++11(三)auto、decltype 与 for
原图
右尖括号>的改进
在 C++98 中,有一条需要程序员规避的规则:如果在实例化模板的时候出现了连续的两个右尖括号>,那么它们之间需要一个空格来进行分隔,以避免发生编译时的错误。
12345template <int i> class X{};template <class T> class Y{};Y<X<1> > x1; //编译成功Y<X<2>> x2; //编译失败
在 x2 的定义中,编译器会把>>优先解析为右移符号。
C98 同样会将>>优先解析为右移。C11 中,这种限制被取消了。事实上,C11 标准要求编译器智能地去判断在哪些情况下>>不是右移符号。使用 C11 标准,上述所示代码则会成功地通过编译。
auto 关键字
auto 的限制
使用 auto 的时候必须对变量进行初始化,这是 auto 的限制之一。那么,除此以外,auto 还有哪些其它的限制呢?
auto 不能在函数的参数中使用
这个应该很容易理解,我们在定义函数的时 ...
C++11(二)右值引用与 POD
原图
继承构造函数
12345678910111213141516struct A{ A(int i){}; A(double d, int i) {}; A(float f, int i, const char* c){}; //...};struct B : A{ B(int i): A(i){}; B(double d, int i): A(d, i){}; B(float f, int i, const char*c): A(f, i, c)(){}; //... virtual void Extrainterface(){};};
继承于 A 的派生类 B 实际上只是添加了一个接口 Extralnterface,那么如果我们在构造 B 的时候想要拥有 A 这样多的构造方法的话,就必须一“透传”各个接口。这无疑是相当不方便的。事实上,在 C++中已经有了一个好用的规则,子类可以通过使用 using 声明来声明继承基类的构造函数。
12345678910111213struct A{ A(int i){}; ...
C++11(一)assert 与 noexcept
原图
静态断言 static_assert
断言 assert 宏只有在程序运行时才能起作用,而#error 只在编译器预处理时才能起作用。有的时候,我们希望在编译时能做一些断言。读者也可以尝试一下 Boost 库内置的 BOOST_STATIC_ASSERT,其缺陷都是很明显的:诊断信息不够充分,从而难以准确定位错误的根源。
在 C++11 标准中,引人了 static_assert 断言来解决这个问题。 static_assert 使用起来非常简单,它接收两个参数,一个是断言表达式,这个表达式通常需要返回一个 bool 值;一个则是警告信息,它通常也就是一段字符串,在出错时会输出该字符信息,这样出错位置就是非常明确的。
noexcept
noexcept 形如其名,表示其修饰的函数不会抛出异常。在 C++11 中如果 noexcept 修饰的函数抛出了异常,编译器可以选择直接调用 std::terminate 函数来终止程序的运行,这比基于异常机制的 throw 在效率上会高一些。这是因为异常机制会带来一些额外开销,比如函数抛出异常,会导致函数栈被依次地展开( unwind ...
重构(三)构筑测试体系
自测试代码的价值
一套测试就是一个强大的 bug 侦测器,能够大大缩减查找 bug 所需的时间。
事实上,撰写测试代码的最好时机是在开始动手编码之前。当我需要添加特性时,我会先编写相应的测试代码。听起来离经叛道,其实不然。编写测试代码其实就是在问自己:为了添加这个功能,我需要实现些什么?编写测试代码还能帮我把注意力集中于接口而非实现(这永远是一件好事)。预先写好的测试代码也为我的工作安上一个明确的结束标志:一旦测试代码正常运行,工作就可以结束了。
Kent Beck 将这种先写测试的习惯提炼成一门技艺,叫测试驱动开发(Test-Driven Development,TDD)[mf-tdd]。测试驱动开发的编程方式依赖于下面这个短循环:先编写一个(失败的)测试,编写代码使测试通过,然后进行重构以保证代码整洁。这个“测试、编码、重构”的循环应该在每个小时内都完成很多次。这种良好的节奏感可使编程工作以更加高效、有条不紊的方式开展。
第一组重构
提炼函数(Extract Function)
我会浏览一段代码,理解其作用,然后将其提炼到一个独立的函数中,并以这段代码的用途为这个函数命名。 ...
重构(二)代码的坏味道
原图
神秘命名(Mysterious Name)
整洁代码最重要的一环就是好的名字,所以我们会深思熟虑如何给函数、模块、变量和类命名,使它们能清晰地表明自己的功能和用法。
然而,很遗憾,命名是编程中最难的两件事之一。正因为如此,改名可能是最常用的重构手法,包括改变函数声明(用于给函数改名)、变量改名、字段改名等。很多人经常不愿意给程序元素改名,觉得不值得费这个劲,但好的名字能节省未来用在猜谜上的大把时间。
改名不仅仅是修改名字而已。如果你想不出一个好名字,说明背后很可能潜藏着更深的设计问题。为一个恼人的名字所付出的纠结,常常能推动我们对代码进行精简。
重复代码(Duplicated Code)
如果你在一个以上的地点看到相同的代码结构,那么可以肯定:设法将它们合而为一,程序会变得更好。一旦有重复代码存在,阅读这些重复的代码时你就必须加倍仔细,留意其间细微的差异。如果要修改重复代码,你必须找出所有的副本来修改。
最单纯的重复代码就是“同一个类的两个函数含有相同的表达式”。这时候你需要做的就是采用提炼函数提炼出重复的代码,然后让这两个地点都调用被提炼出来的那一段代码。如果重复代码只 ...
重构(一)重构的原则
原图
何谓重构
“重构”这个词既可以用作名词也可以用作动词。
重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
重构的关键在于运用大量微小且保持软件行为的步骤,一步步达成大规模的修改。每个单独的重构要么很小,要么由若干小步骤组合而成。因此,在重构的过程中,我的代码很少进入不可工作的状态,即便重构没有完成,我也可以在任何时刻停下来。
Tip
如果有人说他们的代码在重构过程中有一两天时间不可用,基本上可以确定,他们在做的事不是重构。
在上述定义中,我用了“可观察行为”的说法。它的意思是,整体而言,经过重构之后的代码所做的事应该与重构之前大致一样。这个说法并非完全严格,并且我是故意保留这点儿空间的:重构之后的代码不一定与重构前行为完全一致。比如说,提炼函数会改变函数调用栈,因此程序的性能就会有所改变;改变函数声明和搬移函数等重构经常会改变模块的接口。不过就用户应该关心的行为而言,不应该有任何改变。如果我在重构过程中发现了任何 bug,重构 ...
