前言
policy-based programming 是一个超级大的话题。我没有准备好要开启这么大的工程(去谈及它)。
这次是为了延伸上一篇提到的 Lockable policy 话题,而在元编程的 policy 范式范围中做一些回忆。
想到啥就说啥,别当真。
Policy in metaprogramming
Policy
在模板元编程中有一种被称为 Policy 的技法,它隐含了 Java 或 Golang 的 interface 的能力。换句话说,它是一种约定了一组方法的手段,如果你的 class 提供了这组方法的话,那么就叫做满足约定。
严格地说,这种“满足接口约定”是一种最终呈现,其实现机理也并非由 policy pattern 这种技法来提供的,而是 template metaprogramming 的一种编译法则,它是待决名 dependent name 的一种查找规则。
但是由于 policy 的实现效果与 Java 的接口的一定程度的相似性,所以我就混合到一起来成文了。
和这种技法相对应的是 C++ 语言中的纯虚函数,或者 interface。
是的,C++ 也有 interface,只不过它是扩展性的,MSVC(至少从 VC6 开始) 提供了这一支持,一般被用于 IDL/MIDL 开发场景。
此外,作为非标准拓展,interface 关键字常常都是可用的(在 gcc,clang 中也可以,但不一定是你想要的那种方式:C++ Interface (Using the GNU Compiler Collection (GCC)))。
纯虚函数方式
纯虚函数基本上是 C++ 语言层面上能够支持接口的唯一方法,符合标准的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class IDemo
{
public:
virtual ~IDemo() {}
virtual void OverrideMe() = 0;
};
class Parent
{
public:
virtual ~Parent();
};
class Child : public Parent, public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
};
一个纯虚函数形如 virtual void func() = 0
,= 0
表示 pure,此时不能提供函数体,不必提供一个就地的实现,virtual
往往不是必须的,但出于惯例,从来都会被在此时提供。
在 C++98/或者 C++11 之后,虚析构函数被明确了,所以当你在可能使用多态、或者当你定义了纯虚函数接口时,你必须显式地声明虚析构函数,这能够避免通过基类指针删除对象时的未定义行为。
BTW,UB 常常很无聊。
Policy 方式
有时候,Policy Pattern 是很含混的。
这里有一个例子(来自于这里)但做了改进和完善:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct InkPen {
void Write() {
this->WriteImplementation();
}
void WriteImplementation() {
std::cout << "Writing using a inkpen" << std::endl;
}
};
struct BoldPen {
void Write() {
std::cout << "Writing using a boldpen" << std::endl;
}
};
template<class PenPolicy>
class Writer : private PenPolicy {
public:
void StartWriting() {
PenPolicy::Write();
}
};
void test_policy_1() {
Writer<InkPen> writer;
writer.StartWriting();
Writer<BoldPen> writer1;
writer1.StartWriting();
}
它展示了 Policy 的一种应用方法:你不必知道 duck 是谁,你只需要知道 duck 能够做什么。在这个例子中,PenPolicy 类的实体(实参)必须要能够 void Write()
,否则编译期就会报错。
但是 Policy 并不被限制于你要做一个派生类才能实施这样的约定,在成员函数或者函数中也可以:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <class PenPolicy>
class Writer {
PenPolicy pt;
public:
Writer(PenPolicy&& pt_): pt(std::move(pt_)){}
~Writer(){}
void StartWriting(){
pt.Write();
}
}
void test() {
BoldPen bp{};
Writer writer(bp);
writer.StartWriting();
}
这种方式类似于 std::lock_guard 的实现机制,但严格地说它可能已经不是 policy pattern 了。这一点关系不大,只有做学术的才分的那么清,对吧,我特么全都要。
对于你的类以及类体系来说,该怎么设计是一种综合考量的问题,这取决于你的类库想要达到什么样的目的,遵循什么样的风格。
小小结
我们已经知道了 Policy 模式的核心价值在于提供 duck type 的解耦能力。但实际上你需要了解到的最重要一点是,如前所述,解耦能力是由元编程技术提供的,并不是 policy pattern 专属,而本质上 policy pattern 是什么,它是一种向某一父类注入新的行为能力的一种 pattern:例如 BoldPen 在经由 Writer 的 wrap 之后,现在有了 StartWriting() 这一新的能力。
所以,按照这一理解,你能发现像 STL 中的 allocator 等等,都是 policy 手法的体现。
所谓的父类,你应该将其理解为 template <class T> class TMP
中的 TMP
。而注入,你可以将其理解为 template <class _T, class allocator = allocator<_T> > class vector { ... };
中的 allocator
。
当我们采用 Writer 的视角时,我们可以说,我们正在期待的 PenPolicy 必须是支持 void write()
的。在这个意义上,Policy pattern 可以被认为是一种完全解耦的 interface,如同 Golang 中的 interface 那样。在这一点上,Java 甚至还有所不如,因为它家的 interface 是强耦合的。
按照这样的定义,policy 范式就被大大延展了。它不仅仅代表着某一种或者几种惯用手法,而是形成了一个体系,这就是后来以及 C++11写作基本概念的 policy-based programming 风格。
Traits
Policy 技法常常被与 Traits 一起提及,并被比较。Traits 也是模板元编程中的一种技法,这和 Rust 中的 Traits 是不同的,并且 Traits 本身(以及 Policy)出现的时间也早于 Rust 这门语言。
Traits 的目的在于从给定的类中抽出特定的属性,在标准库头文件 type_traits 中有大量的演示。
一个典型的 Traits 可能是这样的:
1
2
3
4
5
6
template< class T >
struct is_integral
{
static const bool value /* = true if T is integral, false otherwise */;
typedef std::integral_constant<bool, value> type;
};
它可以检测 T 是不是一个整数类型。
而进一步的多值 Traits 的典范是 iterator:
1
2
3
4
5
6
7
8
9
template<T>
struct iterator_traits<T*>
{
using difference_type = std::ptrdiff_t;
using value_type = T;
using pointer = T*;
using reference = T&;
using iterator_category = std::random_access_iterator_tag;
};
它的目的在于为一个 iterator 类 T 提供/包装一组原型类型,这些类型将被用在容器类中,于是容器类的算法实现不必在 T 还是 T* 上伤脑筋,算法实现中都是用 value_type 和 pointer 这样的统一的类型名就可以了。在 STL 的实现中,会大量定义和用到统一的类型名,因为这样类的实现算法部分就不必考虑 T 究竟是 who 了。通常在一个类的一开始就会定义一堆类型,而 iterator_traits 则是容器类如 vertor 或 deque 等所使用的场所。
除了上面所描述的 type traits 之外,也有 value traits。实际上我们的第一个示例中,is_integral::value
就是一个 value traits。而且相似的做法在标准库中非常多,std::enable_if::value
就是一个好例子。
收
Traits 只是顺便的顺便,在这里小小提一下,不再展开了。
小结
policy-based design 是现代 C++ 编程所推崇的理念之一。所谓的 Modern C++ Design 不但是一种 C++11 以来的 理念,实际上也正是源于早前的同名书籍的酝酿,该书有译本《C++设计新思维》。
policy 本身作为一种技法手段,在上世纪 90 年代就已经出现,并且在多个类库(MFC,OWL 等,不够精确,不易考证了,Prof-UIS C++ Library for MFC 等)中有所实作,并且曾有文章(可能是在 Code Project 上),但当时或许往往并不被刻意归纳和总结并命名为 xxxPolicy。不过后来在神库 Loki 中它就完完全全起势了,跟随 Modern C++ Design 一书所获的的一系列关注最终将编码理念推进了一大步。Loki 所制造的锁定语义(即 ClassLevelLockable 与 ObjectLevelLockable),其实也正是后来 C++11 中的 BasicLockable,这个现在被译作“基本可锁定”,我也是一种 #$%^@&。实际上,C++11 标准在很大程度上是 Loki 和 Boost 的催生物。
一般的认识,Loki 比 boost 出生的更早。
不过,这是很难考证的事情。因为从上世纪 80 年代起,C++ 开发一直处于绝对强势的地位。20 年来的技法探讨、工程实践促使了 loki 和 boost 原始版本的出世,从 70 年代起约 40 年的实践才带来了 C++11 的标准面世,所以你不太可能区分出这两者究竟谁更早,在 2001 左右的大家都受到过相似的思潮(Turbo Vision,SGI STL 等等)培育。考虑到 boost 有着 2001 以来的一系列发展和逐渐完善的过程,且其首版(约1999)甚至连智能指针都没有,所以通常会认为它(现在人们所认识的那个 boost)比较于 loki 更晚一点。
https://web.archive.org/web/19991103202539/http://www.boost.org/
再结
回想我对 template 的认知,也是遵循这样的过程:
- 这个类怪通用的,给它戴上
template <class T>
的前缀,然后替换一下类型到 T。好了,搞定收工。 - 再到初识 policies。一开始时将其当成策略来理解的。
- 等到对组装爱不释手时,我才明白 template 并不是泛泛的类型,
可以这么说,只有在 Loki 为代表的现代 C++ 风格上能够做出优秀的组装工件的程序员,才能算是好手。既然在早几十年前没有那样的 C++ 编译器,那么那时候的如云好手们自不必受此约束。
元编程是高精尖吗?
不过,如果我们不是对元编程抱有那么大的敬畏的话,以通用的眼光去看待编程能力,则你不必具有元编程能力才能被称作好手。元编程是在普通编程方法上加上一层编译过程,如果说普通编程是我编写代码并预判其运行效果的话,那么元编程则是我编写代码并预判其“生成”的代码,然后这些“生成”的代码将会有怎样的运行效果就是整个元编程套路的呈现了。所以如果你已经入了编程的大门的话,元编程是顺理成章的,一个好手自然不可能在元编程上无所建树。
因为耽搁,结果拖了两天,回头审视这篇文章,感觉不如意处甚多。不过即使是思考没有整理的太好,表述不怎么达意,也无所谓了。
因为我也无所求。
留下评论