10.5 编程风格
针对大多数现实问题的最佳解决方案需要组合使用多种技术,这也是 C++ 演进的主要动力。自然地,这让那些声称拥有单个简单最佳解决方案(编程范式
)的人感到不爽,但是支持多种风格一直是 C++ 的根本优势。考虑一下绘制所有形状
的例子,这个例子自 Simula 发展早期(绘图设备为湿墨绘图仪)以来就一直用于说明面向对象编程。用 C++20,我们可以这样写:
void draw_all(range auto& seq)
{
for (Shape& s : seq)
s.draw();
}
该段代码是什么编程范式?
- 显然,它是面向对象编程:使用了虚函数和类层次结构。
- 显然是泛型编程:使用了模板(通过使用
range
概念进行参数化,我们得到一个模板)。 - 显然,这是普通的命令式编程:使用了
for
循环,并按照常规f(x)
语法定义了一个将要被调用的函数。
对这个例子我可以进一步展开:Shape
通常具有可变状态;我可以使用 lambda 表达式,也可以调用 C 函数;我可以用 Drawable
的概念对参数进行更多约束。对于各种更好
的定义,适当的技术组合比我能想到的任何一种单一范式所能提供的解决方案更好。
C++ 支持多种编程风格(如您坚持,也可以称为范式
),其背后的想法并不是要让我们选择一种最喜欢的样式进行编程,而是可以将多种风格组合使用,以表达比单一风格更好的解决方案。
10.5.1 泛型编程
在 2006 年,许多 C++ 代码仍然是面向对象的风格和 C 风格编程的混合体。自然而然的,到 2020 年仍然有很多类似这样的代码。但是,随着 C++98 的到来,STL 风格的泛型编程(通常称为 GP)变得广为人知,并且用户代码也逐渐开始使用 GP,而不只是简单地使用标准库。C++11 中对 GP 的更好支持为在生产代码中更广泛的使用 GP 提供了极大的便利。但是,C++17 中缺少概念(§6),这仍然阻碍了 C++ 中泛型编程的使用。
基本上,所有专家都阅读过 Alex Stepanov 的《编程原本》(Elements of Programming,通常称为 EoP)[Stepanov and McJones 2009],并受到其影响。
基于模板的泛型编程是 C++ 标准库的支柱:容器、范围(§9.3.5)、算法、iostream、文件系统(§8.6)、随机数(§4.6)、线程(§4.1.2)(§9.4)、锁(§4.1.2)(§8.4)、时间(§4.6)(§9.3.6)、字符串、正则表达式(§4.6)和格式化(§9.3.7)。
10.5.2 元编程
C++ 中的元编程出自泛型编程,因为两者都依赖于模板。它的起源可以追溯到 C++ 模板的早期,当时人们发现模板是图灵完备的 [Vandevoorde and Josuttis 2002; Veldhuizen 2003],并以某种有用的形式提供编译期纯函数式编程。
模板元编程(通常称为 TMP)往往非常丑。有时,这种丑陋通过使用宏来掩盖,从而造成了其他问题。TMP 几乎无处不在,这也证明了它确实有用。例如,如果没有元编程,就无法实现 C++14 标准库。许多技巧和实验在 2006 年前就有了,但是 C++11 具有更好的编译器、变参模板(§4.3.2)和 lambda 表达式(§4.3.1),这些推动了 TMP 成为主流用法。C++ 标准库还增加了更多元编程的支持,比如:编译期选择模板 conditional
,允许代码依赖于类型属性的类型特征(type trait)如能否安全地按位复制类型 X?
(§4.5.1),还有 enable_if
(§4.5.1)。举例来说:
conditional<(sizeof(int)<4),double,int>::type x; // 如果 int 小,就用 double
计算类型以精确地反映需求,这可以说是 TMP 的本质。我们还可以计算值:
template <unsigned n>
struct fac {
enum { val = n * fac<n-1>::val };
};
template <>
struct fac<0> { // 0 的特化:fac<0> 为 1
enum { val = 1 };
};
constexpr int fac7 = fac<7>::val; // 5040
注意,模板特化在其中起着关键作用,这一点在大多数 TMP 中是必不可少的。它已用于计算复杂得多的数值,也可以表示控制流(例如,在编译期计算决策表,进行循环展开,等等)。在 C++98 [Stroustrup 2007] 中,模板特化是一个很大程度上没有得到足够重视的特性。
在设计精巧的库中以及在现实世界的代码中,诸如 enable_if
之类的原语已成为数百甚至数千行的程序的基础。TMP 的早期示例包含一个完整的编译期 Lisp 解释器 [Czarnecki and Eisenecker 2000]。此类代码极难调试,而维护它们更是可怕的差事。我见识过这样的情形,几百行基于 TMP 的代码(不得不承认非常聪明),在一台 30G 内存的计算机上编译需要好几分钟的时间,由于内存不足而导致最终编译失败。即使是简单的错误,编译器的错误信息也可以达到几千行。然而,TMP 仍被广泛使用。理智的程序员发现,尽管 TMP 有着各种问题,仍比起其他方案要好。我见过 TMP 生成的代码比我认为一个合格的人类程序员会手写的汇编代码要更好。
因此,问题变成了如何更好地满足这种需求。当人们开始把像 fac<>
这样的代码视为正常时,我为此而感到担心。这不是表达普通数值算法的好方法。概念(§6)和编译期求值函数(constexpr
(§4.2.7))可以大大简化元编程。举例来说:
constexpr int fac(int n)
{
int r = 1;
while (n>1) r*=n--;
return r;
};
constexpr int fac7 = fac(7); // 5040
这个例子说明,当我们需要一个值时,函数是最佳的计算方式,即使——尤其——在编译期。传统模板元编程最好只保留用于计算新的类型和控制结构。
Jaakko Järvi 的 Boost.Lambda [Järvi and Powell 2002; Järvi et al. 2003a] 是 TMP 的早期使用案例,它帮助说服了人们 lambda 表达式是有用的,并且他们需要直接的语言支持。
Boost 元编程库 Boost.MPL [Gurtovoy and Abrahams 2002–2020] 展示了传统 TMP 的最好和最坏的方面。更现代的库 Boost.Hana [Boost Hana 2015–2020] 使用 constexpr
函数。WG21 的 SG7(§3.2)试图开发一种更好的标准元编程系统,其中还包括编译期反射(§9.6.2)。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论