介绍

现代C++设计

Modern C++ Design: Generic Programming and Design Patterns Applied是Andrei Alexandrescu撰写的一本书, Addison-Wesley于 2001 年出版。它被Scott Meyers视为“最重要的 C++ 书籍之一”。[1]

现代 C++ 设计

本书利用并探索了一种称为模板元编程的C++编程技术。虽然Alexandrescu 没有发明这项技术,但他已经在程序员中普及了它。他的书包含 C++ 程序员可能面临的实际问题的解决方案。本书中的几个短语现在在 C++ 社区中用作通用术语:现代C++(与 C/C++ 风格相反)、基于策略的设计和类型列表。

书中描述的所有代码都可以在他的库Loki中免费获得。自 2001 年以来,该书已再版并翻译成多种语言。

基于策略的设计

基于策略的设计,也称为基于策略的类设计或基于策略的编程,是现代 C++ 设计中使用的术语,用于基于称为策略的 C++习语的设计方法。它被描述为策略模式的编译时变体,并与 C++模板元编程有关。它最初是由Andrei Alexandrescu在C/C++ 用户期刊中的现代 C++ 设计和他的专栏Generic<Programming>中在 C++ 中推广的,目前它与 C++ 和D密切相关因为它需要一个对模板具有高度强大支持的编译器,这在 2003 年左右之前并不常见。

这种设计方法的先前示例基于参数化的通用代码,包括ML语言的参数模块(函子)、[2]和用于内存管理策略的C++分配器。

基于策略的设计的中心习惯用法是一个类模板(称为宿主类),将几个类型 参数作为输入,这些参数使用用户选择的类型(称为策略类)进行实例化,每个都实现一个特定的隐式接口(称为policy),并封装实例化的宿主类行为的一些正交(或大部分正交)方面。通过为每个策略提供与一组不同的、固定的实现相结合的宿主类,库或模块可以支持不同行为组合的指数数量,在编译时解决,并通过在宿主类模板的实例化中混合和匹配不同提供的策略类来选择。此外,通过编写给定策略的自定义实现,基于策略的库可以用于需要库实现者无法预见的行为的情况。即使在每个策略的实现不超过一个的情况下,将一个类分解为策略也可以通过增加模块化和突出显示正交设计决策的确切位置来帮助设计过程。

虽然从可互换的模块中组装软件组件并不是一个新概念,但基于策略的设计代表了一种创新,它在定义单个类的行为的(相对较低的)级别应用该概念的方式。策略类与回调有一些相似之处,但不同之处在于,策略类不是由单个函数组成,而是通常包含几个相关的函数(方法),通常与状态 变量或其他工具(如嵌套类型)结合使用。基于策略的主机类可以被认为是一种元功能,将一组由类型表示的行为作为输入,并返回一个表示将这些行为组合成一个功能整体的结果的类型作为输出。(然而,与MPL元函数不同,输出通常由实例化的宿主类本身表示,而不是嵌套的输出类型。)

策略惯用语的一个关键特征是,通常(尽管并非绝对必要),宿主类将使用(公共)多重继承从其每个策略类派生(使自己成为子类) 。(替代方案是宿主类仅包含每个策略类类型的成员变量,或者私有继承策略类;但是公开继承策略类的主要优点是策略类可以添加新方法,由实例化的宿主类并可供其用户访问,宿主类本身甚至不需要知道。)策略习语的这一方面的一个显着特征是,相对于面向对象的编程, 策略颠倒了基类和派生类之间的关系——而在 OOP 中,接口传统上由(抽象)基类表示,接口的实现由派生类表示,在基于策略的设计中,派生(主机)类表示接口和基类(策略)类实现它们。在策略的情况下,公共继承并不代表主机和策略类之间的 is-a 关系。虽然这通常被认为是OOP 上下文中设计缺陷的证据,但这并不适用于策略习语的上下文。

当前版本的策略的一个缺点是策略接口在代码中没有直接、显式的表示,而是通过鸭子类型隐式定义的,并且必须在注释中单独手动记录。主要思想是利用共性-可变性分析将类型划分为固定的实现和接口、基于策略的类和不同的策略。诀窍是知道什么进入主类,以及应该创建什么策略。上面提到的文章给出了以下答案:无论我们需要做出可能的限制性设计决策,我们都应该推迟该决策,我们应该将其委托给一个适当命名的策略。

策略类可以包含实现、类型定义等。基本上,主模板类的设计者将定义策略类应该提供什么,他们需要实现哪些定制点。

制定一套好的政策,只有正确的数量(例如,最低限度的必要),这可能是一项微妙的任务。不同的定制点,同属一个,应该归入一个策略参数,例如存储策略、验证策略等。平面设计师能够为他们的策略命名,它代表概念,而不是代表操作或次要实现细节的策略。

基于策略的设计可以结合其他有用的技术。例如,模板方法模式可以在编译时重新解释,以便主类具有骨架算法,该算法在定制点调用某些策略的适当函数。

这将通过未来版本的 C++ 中的概念[3]动态实现。

简单示例

下面介绍的是一个 C++ hello world 程序的简单(人为)示例,其中要打印的文本及其打印方法使用策略进行分解。在此示例中,HelloWorld是一个主机类,它采用两个策略,一个用于指定应如何显示消息,另一个用于指定要打印的实际消息。请注意,通用实现已在其中,因此除非同时提供策略 (和),Run否则无法编译代码。

#include <iostream>
#include <string>

template <typename OutputPolicy, typename LanguagePolicy
class HelloWorld : private OutputPolicy, private LanguagePolicy {
 public:
  // Behavior method.
  void Run() const {
// Two policy methods.
Print(Message());
  }

 private:
  using LanguagePolicy::Message
  using OutputPolicy::Print
};

class OutputPolicyWriteToCout {
 protected:
  template <typename MessageType
  void Print(MessageType&& message) const {
std::cout << message << std::endl
  }
};

class LanguagePolicyEnglish {
 protected:
  std::string Message() const { return "Hello, World!" }
};

class LanguagePolicyGerman {
 protected:
  std::string Message() const { return "Hallo Welt!" }
};

int main() {
  // Example 1
  using HelloWorldEnglish = HelloWorld<OutputPolicyWriteToCout, LanguagePolicyEnglish

  HelloWorldEnglish hello_world
  hello_world.Run();  // Prints "Hello, World!".

  // Example 2
  // Does the same, but uses another language policy.
  using HelloWorldGerman = HelloWorld<OutputPolicyWriteToCout, LanguagePolicyGerman

  HelloWorldGerman hello_world2
  hello_world2.Run();  // Prints "Hallo Welt!".
}

设计者可以OutputPolicy通过添加带有成员函数的新类轻松地编写更多的 s,并将Print其作为新OutputPolicy的 s。

Loki库

Loki是Andrei Alexandrescu在他的《现代 C++ 设计》一书中编写的C++ 软件库的名称。

该库广泛使用 C++模板元编程并实现了几个常用工具:typelist、functor、singleton、smart pointer、object factory、visitor和multimethods。

最初,该库仅与两种最符合标准的 C++ 编译器(CodeWarrior和Comeau C/C++)兼容:后来的努力使其可用于各种编译器(包括较旧的Visual C++ 6.0、Borland C++ Builder 6.0、Clang和GCC)。编译器供应商使用 Loki 作为兼容性基准,进一步增加了兼容编译器的数量。[4]

Loki 的维护和进一步开发一直通过由Peter Kümmel和Richard Sposato领导的开源社区作为SourceForge 项目继续进行。许多人的持续贡献提高了库的整体健壮性和功能。Loki 不再与本书绑定,因为它已经有很多新组件(例如 StrongPtr、Printf 和 Scopeguard)。Loki 启发的类似工具和功能现在也出现在Boost库集合中。

前言

C++的一些高级特性对于新人来说,很具有挑战性,而模板就是其中之一,晦涩语法让很多新人望而生畏;大多数人苦苦磨炼,却始终没有掌握这门绝学,本文通过揭开模板的一些面纱,希望帮助新人掌握模板的心法,从而学会这门武功(技术),助你跨过C++这座大山,向C++顶级程序员迈进,升职加薪;

C++模版的诞生

程序 = 数据结构 + 算法 ---Niklaus EmilWirth                     

程序本质是数据结构+算法,任何一门语言都可以这样理解,这个公式对计算机科学的影响程度足以类似物理学中爱因斯坦的“E=MC^2”——一个公式展示出了程序的本质。

最初C++是没有标准库的,任何一门语言的发展都需要标准库的支持,为了让C++更强大,更方便使用,Bjarne Stroustrup觉得需要给C++提供一个标准库,但标准库设计需要一套统一机制来定义各种通用的容器(数据结构)和算法,并且能很好在一起配合,这就需要它们既要相对的独立,又要操作接口保持统一,而且能够很容易被别人使用(用到实际类中),同时又要保证开销尽量小(性能要好)。Bjarne Stroustrup 提议C++需要一种机制来解决这个问题,所以就催生了模板的产生,最后经标准委员会各路专家讨论和发展,就发展成如今的模版, C++ 第一个正式的标准也加入了模板。

C++模版是一种解决方案,初心是提供参数化容器类和通用的算法(函数),目的就是为了减少重复代码,让通用性和高性能并存,提高C++程序员生产力。

什么是参数化容器类?

首先C++是可以提供OOP(面向对象)范式编程的语言,所以支持类概念,类本身就是现实中一类事物的抽象,包括状态和对应的操作,打个比喻,大多数情况下我们谈论汽车,并不是指具体某辆汽车,而是某一类汽车(某个品牌),或者某一类车型的汽车。

 

所以我们设计汽车这个类的时候,各个汽车品牌的汽车大体框架(骨架)都差不多,都是4个轮子一个方向盘,而且操作基本上都是相同的,否则学车都要根据不同厂商汽车进行学习,所以我们可以用一个类来描述汽车的行为:

class Car
{
public:
Car(...);
//other operations
...
private:
Tire m_tire[4];
Wheel m_wheel;
//other attributes
...
};

但这样设计扩展性不是很好,因为不同的品牌的车,可能方向盘形状不一样,轮胎外观不一样等等。所以要描述这些不同我们可能就会根据不同品牌去设计不同的类,这样类就会变得很多,就会产生下面的问题:

1. 代码冗余,会产生视觉复杂性,本身相似的东西比较多;

2. 用户很难通过配置去实现一辆车设计,不好定制化一个汽车;

3. 如果有其中一个属性有新的变化,就得实现一个新类,扩展代价太大。

 

这个时候,就希望这个类是可以参数化的(属性参数化),可以根据不同类型的参数进行属性配置,继而生成不同的类。类模板就应运而生了,类模板就是用来实现参数化的容器类。

什么是通用算法?

程序=数据结构+算法

算法就是对容器的操作,对数据结构的操作,一般算法设计原则要满足KISS原则,功能尽量单一,尽量通用,才能更好和不同容器配合,有些算法属于控制类算法(比如遍历),还需要和其他算法进行配合,所以需要解决函数参数通用性问题。举个例子, 以前我们实现通用的排序函数可能是这样:

void sort (void* first, void* last, Cmp cmp);

这样的设计有下面一些问题:

1. 为了支持多种类型,需要采用void*参数,但是void*参数是一种类型不安全参数,在运行时候需要通过类型转换来访问数据。

2. 因为编译器不知道数据类型,那些对void*指针进行偏移操作(算术操作)会非常危险(GNU支持),所以操作会特别小心,这个给实现增加了复杂度。

 

所以要满足通用(支持各种容器),设计复杂度低,效率高,类型安全的算法,模板函数就应运而生了,模板函数就是用来实现通用算法并满足上面要求。

C++模板的实现

C++标准委员会采用一套类似函数式语言的语法来设计C++模板,而且设计成图灵完备 (Turing-complete)(详见参考),我们可以把C++模板看成是一种新的语言,而且可以看成是函数式编程语言,只是设计依附在(借助于)C++其他基础语法上(类和函数)。

C++实现类模板(class template)技术

1.定义模板类,让每个模板类拥有模板签名。

template<typename Tclass X{...};

上面的模板签名可以理解成:X<typename T>; 主要包括模板参数<typename T>和模板名字X(类名), 基本的语法可以参考《C++ Templates: The Complete Guide》,《C++ primer》等书籍。

模板参数在形式上主要包括四类,为什么会存在这些分类,主要是满足不同类对参数化的需求:

  • type template parameter: 类型模板参数,以class或typename 标记;此类主要是解决朴实的参数化类的问题(上面描述的问题),也是模板设计的初衷。

  • non-type template parameter: 非类型模板参数,比如整型,布尔,枚举,指针,引用等;此类主要是提供给大小,长度等整型标量参数的控制,其次还提供参数算术运算能力,这些能力结合模板特化为模板提供了初始化值,条件判断,递归循环等能力,这些能力促使模板拥有图灵完备的计算能力。

  • template template parameter,模板参数是模板,此类参数需要依赖其他模板参数(作为自己的入参),然后生成新的模板参数,可以用于策略类的设计policy-base class。

  • parameter pack,C++11的变长模板参数,此类参数是C++11新增的,主要的目的是支持模板参数个数的动态变化,类似函数的变参,但有自己独有语法用于定义和解析(unpack),模板变参主要用于支持参数个数变化的类和函数,比如std::bind,可以绑定不同函数和对应参数,惰性执行,模板变参结合std::tuple就可以实现。

     

2. 在用模板类声明变量的地方,把模板实参(Arguments)(类型)带入模板类,然后按照匹配规则进行匹配,选择最佳匹配模板. 模板实参和形参类似于函数的形参和实参,模板实参只能是在编译时期确定的类型或者常量,C++17支持模板类实参推导。

 

3. 选好模板类之后,编译器会进行模板类实例化--记带入实际参数的类型或者常量自动生成代码,然后再进行通常的编译。

TMP模板元编程Template metaprogramming

随着模板技术发展,模板元编程逐渐被人们发掘出来,metaprogramming本意是进行源代码生成的编程(代码生成器),同时也是对编程本身的一种更高级的抽象,好比我们元认知这些概念,就是对学习本身更高级的抽象。TMP通过模板实现一套“新的语言”(条件,递归,初始化,变量等),由于模板是图灵完备,理论上可以实现任何可计算编程,把本来在运行期实现部分功能可以移到编译期实现,节省运行时开销,比如进行循环展开,量纲分析等。

Policy-Based Class Design基于策略的多继承类模板设计模式

C++ Policy class design 首见于 Andrei Alexandrescu 出版的 《Modern C++ Design》一书以及他在C/C++ Users Journal杂志专栏 Generic<Programming>,参考wiki。通过把不同策略设计成独立的类,然后通过模板参数对主类进行配置,通常policy-base class design采用继承方式去实现,这要求每个策略在设计的时候要相互独立正交。STL还结合CRTP (Curiously recurring template pattern)等模板技术,实现类似动态多态(虚函数)的静态多态,减少运行开销。

C++模板的核心技术

1. SFINAE -Substitution failure is not an error 

要理解这句话的关键点是failure和error在模板实例化中意义,模板实例化时候,编译器会用模板实参或者通过模板实参推导出参数类型带入可能的模板集(模板备选集合)中一个一个匹配,找到最优匹配的模板定义,

Failure:在模板集中,单个匹配失败;

Error:在模板集中,所有的匹配失败;

所以单个匹配失败,不能报错误,只有所有的匹配都失败了才报错误。

2. 模板特化

模板特化为了支持模板类或者模板函数在特定的情况(指明模板的部分参数(偏特化)或者全部参数(完全特化))下特殊实现和优化,而这个机制给与模板某些高阶功能提供了基础,比如模板的递归(提供递归终止条件实现),模板条件判断(提供true或者false 条件实现)等。

3. 模板实参推导

模板实参推导机制给与编译器可以通过实参去反推模板的形参,然后对模板进行实例化,具体推导规则见参考;

4. 模板计算

模板参数支持两大类计算:

一类是类型计算(通过不同的模板参数返回不同的类型),此类计算为构建类型系统提供了基础,也是泛型编程的基础; 一类是整型参数的算术运算, 此类计算提供了模板在实例化时候动态匹配模板的能力;实参通过计算后的结果作为新的实参去匹配特定模板(模板特化)。

5. 模板递归

模板递归是模板元编程的基础,也是C++11变参模板的基础。

Generic Programming泛型编程

由于模板这种对类型强有力的抽象能力,能让容器和算法更加通用,这一系列的编程手法,慢慢引申出一种新的编程范式:泛型编程。泛型编程是对类型的抽象接口进行编程,STL库就是泛型编程经典范例。

C++模版的应用场景

1. C++ Library:

 

可以实现通用的容器(Containers)和算法(Algorithms),比如STL,Boost等,使用模板技术实现的迭代器(Iterators)和仿函数(Functors)可以很好让容器和算法可以自由搭配和更好的配合;

 

2.  C++ type traits

 

通过模板技术,C++ type traits实现了一套操作类型特性的系统,C++是静态类型语言,在编译时候需要对变量和函数进行类型检查,这个时候type traits可以提供更多类型信息给编译器, 能让程序做出更多策略选择和特定类型的深度优化,Type Traits有助于编写通用、可复用的代码。

C++创始人对traits的理解:

"Think of a trait as a small object whose main purpose is to carry information used by another object or algorithm to determine "policy" or "implementation details". - Bjarne Stroustrup

 

而这个技术,在其他语言也有类似实现,比如go的interface,java的注解,反射机制等。

C++模版的展望

1. 模版的代价

 

没有任何事物是完美的,模板设计如此精良也有代价的,模板的代码和通常的代码比起来,

  • 代码可读性差,理解门槛高

    一般人初学者很难看懂,开发和调试比较麻烦,对人员要求高,是跨越C++三座大山之一;

  • 代码实现稳定性代价大

    对模板代码,实际上很难覆盖所有的测试,为了保证代码的健壮性,需要大量高质量的测试,各个平台(编译器)支持力度也不一样(比如模板递归深度,模板特性等),可移植性不能完全保证。模板多个实例很有可能会隐式地增加二进制文件的大小等,所以模板在某些情况下有一定代价,一定要在擅长的地方发挥才能;

     

如何降低门槛,对初学者更友好,如何降低复杂性,这个是C++未来发展重要的方向。现代c++正在追求让模板,或者说编译期的计算和泛型约束变简单,constexpr,concept,fold expression,还有C++ 20一大堆consteval,constinit,constexpr virtual function,constexpr dynamic cast,constexpr container等等特性的加入就是为了解决这些问题。曾经的递归变成了普通的constexpr函数,曾经的SFINAE变成了concept,曾经的枚举常量变成了constexpr常量,曾经的递归展开变成了fold expression,越来越简单,友好了。

 

2. 基于模板的设计模式

 

随着C++模板技术的发展,以及大量实战的经验总结,逐渐形成了一些基于模板的经典设计,比如STL里面的特性(traits),策略(policy),标签(tag)等技法;Boost.MPL库(高级C++模板元编程框架)设计;Andrei Alexandrescu 提出的Policy-Based Class Design;以及Jim Coplien的curiously recurring template pattern (CRTP),以及衍生Mixin技法;或许未来,基于模板可以衍生更多的设计模式,而这些优秀的设计模式可以实现最大性能和零成本抽象,这个也是C++的核心精神。

3. 模板的未来

 

随着模板衍生出来的泛型编程,模板元编程,模板函数式编程等理念的发展,将来也许会发展出更抽象,更通用编程理念。模板本身是图灵完备的,所以可以结合C++其他特性,编译期常量和常量表达式,编译期计算,继承,友元friend等开阔出更多优雅的设计,比如元容器,类型擦除,自省和反射(静态反射和metaclass)等,将来会出现更多优秀的设计。

参考资料

  1. C++模版的本质

  2. [不错的简明模板教程] 02/20 — C++ 模板系列小结07-尾置返回类型

  3. 02/20 — C++ 模板系列小结06-可变参数模板特性

  4. 02/09 — C++ 中的多线程的使用和线程池建设

  5. 02/09 — C++ 模板系列小结05-模板类型作为模板参数

  6. 02/09 — C++ 模板系列小结04-类模板中的成员模板

  7. 02/09 — C++ 模板系列小结03-在模板中指定变量类型

  8. 02/08 — C++ 模板系列小结02-非类型模板参数

  9. 02/08 — C++ 模板系列小结01-函数模板和类模板

  10. Loki是由Andrei编写的一个与《Modern C++ Design》(C++设计新思维)一书配套发行的C++代码库。 它不仅把C++模板的功能发挥到了极致,而且把类似设计模式这样思想层面的东西通过库来提供。

    参考:Why is the Loki library not more widely used? https://stackoverflow.com/questions/2348112/why-is-the-loki-library-not-more-widely-used

    loki  https://github.com/lokicui/loki

    How good is the Loki C++ library? https://www.quora.com/How-good-is-the-Loki-C++-library

    Modern C++有哪些能真正提升开发效率的语法糖? https://www.zhihu.com/question/298981020/answer/531340545   [我就是Modern C++ 程序员,在2005年我们的项目中使用了很多Loki库的内容,当年我的代码中使用了大量的模板元编程,很精巧,表面看起来简洁,但实现极其复杂,就如同恶作剧一样,简单的几行模板元代码生成大量的程序,乍一看很清晰,但理解背后的原理要耗费大量的脑力,模板元编程同时提供了漂亮精简的接口和复杂而烧脑的实现。但是在几年后,Boost这个C++准标准库也提供了类似功能的模板元编程库,更加强大,易用。Loki在和Boost这个灭霸的战斗中,就这样被掐死了。过程中我用所有的Boost模板元库替代Loki,再后来因工作的原因改C#编程,就没机会用模板元了。但我仍旧怀念那段恶作剧的日子,每当我看到漫威宇宙关于洛基的故事的时候,我仍旧会怀念当年的Loki。洛基被灭霸杀死的时候,我也想起了那段从Loki迁移到Boost模板元库的时候。]

    《Modern C++ Design》Loki库源码读解随想 (转)  http://blog.itpub.net/10752043/viewspace-992495/

    重剑无锋,大巧不工www.cppblog.com/lai3d/category/5300.html [常有人询问,编程需要天赋吗?啊,任何事情走往极致,都需要天赋。任何一个软件产品的极致成功,都需要创意天赋、编程天赋、管理天赋、行销天赋……。然而,只需用心模仿,再加一点匠心独具,任何人都能够把编程路走得稳当顺遂。能读千赋则善赋,能观千剑则晓剑,巧者不过习者之门也。你把名家源码融为己用,别人也会赞叹一声“你有编程天赋”。子曾经曰过:编程无他,唯手熟尔!用google的CodeSearch搜Loki::,可以看到实际的使用。]

    Loki库使用(1)https://blog.csdn.net/chollima/article/details/8158580

    读书笔记《C++设计新思维》(2) Int2Type的意义http://www.cppblog.com/tommy/archive/2006/01/24/2996.html