龙空技术网

现代C++金融编程指南:1 C++概述

启辰8 264

前言:

当前兄弟们对“c语言界面库”大致比较关切,各位老铁们都想要学习一些“c语言界面库”的相关知识。那么小编在网摘上收集了一些有关“c语言界面库””的相关文章,希望姐妹们能喜欢,咱们快快来学习一下吧!

在开始使用 C++ 编程之前,先简要概述一下该语言及其姊妹软件 C++ 标准库,以及它在量化金融领域继续发挥重要作用的领域。

这里的一些内容可能对大多数读者来说都很熟悉,但讨论试图将一些基础知识与定量编程相关的要点联系起来,此外还介绍了 C++ 的一些新添加内容。

C++ 和定量金融

C++ 在 20 世纪 90 年代中期开始在金融领域快速发展。 我们这个行业中的许多人都是在 Fortran 环境下长大的,特别是在编写数值例程和科学应用程序方面。 虽然 Fortran 及其支持库——“BLAS、LAPACK、IMSL”——在数学和线性代数支持方面非常发达,但该语言当时缺乏对面向对象编程的支持。

抽象的财务建模自然由相互作用的不同组件组成。 例如,要对基于外汇和利率的简单衍生品合约进行定价,通常需要满足以下条件:

每种货币的收益率曲线

实时外汇报价的市场汇率源

期权定价和风险衡量的波动率曲线和/或曲面

一组衍生品定价方法,例如封闭式或数值近似法

这些组件中的每一个都可以由一个对象来表示,C++ 提供了创建这些对象并管理它们之间的关系的方法。

银行和其他金融机构还需要一种方法来计算区域和全球范围内的风险指标。 对于交易业务遍布纽约、伦敦和东京等全球主要金融中心的公司来说,这是一个重大挑战。 在每个交易日开始时,公司位于纽约的总部都需要进行风险报告,其中考虑到本地和全球维护的投资组合。 这可能是一项计算密集型任务,但 C++ 的性能使其成为可能,并且是其在金融行业早期采用的另一个重要因素。

世纪之交之后,更新的面向对象语言(例如 Java 和 C#)使软件开发变得相对更简单、更快,而更高效的处理器变得更便宜。 然而,这些语言中支持更快部署的相同功能(例如内置托管内存、垃圾收集和中间编译)也可能会带来运行时性能方面的开销。 关于采用哪种语言的管理决策通常取决于更快的开发和运行时效率之间的权衡。 即使采用其中一种语言替代方案,计算密集型定价模型和风险计算仍然“并且仍然”经常委托给现有的 C++ 库并通过接口调用。 还应该注意的是,C++ 提供了其他编程语言所不具备的某些编译时优化功能。

C++11:现代时代的诞生

2011 年,ISO C++ 委员会{1}(通常简称“委员会”)发布了一项重大修订,解决了长期需要的现代化问题,特别是提供了一些非常受欢迎的抽象概念,这些抽象概念对定量开发人员立即有用。 这些包括:

从各种概率分布生成随机数

封装数学函数的 Lambda 表达式也可以作为参数传递

基于基本任务的并发性,可以并行计算,无需手动线程管理(计划在 C++20 之后的未来版本中进一步增强)

智能指针有助于防止与内存相关的程序崩溃

这些主题以及更多内容将在后面的章节中讨论。 Jon Kalb 和 Gašper Ažman {2} 撰写的优秀参考文献涵盖了 C++ 的历史和现代演变。 还应该指出的是,随着委员会对 ISO C++ 核心指南{3}和 ISO C++ 编码标准{4}的更多关注和推广,跨平台开发现在比过去几年容易得多。 本书中将特别频繁地引用核心指南。

继 C++11 之后,具有越来越多现代功能、满足金融和数据科学行业需求的新版本将以三年的节奏推出,最新版本是 C++20。 本书将主要涵盖 C++20 的开发,但我们将讨论 C++23 中的一些项目,并提出金融开发人员应该感兴趣的 C++26 中即将出现的吸引力。

自营交易和高频交易公司一直处于采用 C++11 及更高版本标准的最前沿,其中统计策略中对市场和交易簿信号的反应速度可能意味着利润和损失的巨大差异。 现代 C++ 也迫切需要实现投资银行和对冲基金的交易员和风险经理所使用的计算密集型衍生品定价模型。

开源数学库

过去十年中另一个非常受欢迎的发展是用标准 C++ 编写的强大的开源数学库的激增,因此不需要过去耗时的 C 语言界面操作。 其中主要是 Boost 库、Eigen 和 Armadillo 线性代数库以及 TensorFlow 和 PyTorch 等机器学习库。 随着本书的进展,我们将进一步讨论其中的一些内容,特别是 Boost 和 Eigen。

关于 C++ 的一些误解

关于 C++ 的一些更臭名昭著的信念是:

学习 C++ 需要 C 知识

虽然 C++ 标准保留了 C 语言的大部分内容,但完全有可能在没有 C 知识的情况下学习 C++,正如我们将在下面的材料中看到的那样。 坚持 C 风格实际上会阻碍学习 C++ 的强大抽象和潜在优势。

C++太难了

好吧,是的,这有一定道理,因为毫无疑问,C++ 是一种丰富的语言,它提供了大量众所周知的绳索,可以让人们上吊,而且有时它确实会成为重大挫折的根源。 然而,通过利用该语言的现代功能,同时从一开始就搁置遗留问题,作为一名 C++ 定量金融开发人员,完全有可能相当快地变得非常高效。

内存泄漏始终是 C++ 中的一个问题

有了自 C++11 以来可用的智能指针——“较新的现代功能之一”——以及其他标准库功能,例如 STL 算法(将在第 4 章中介绍),这在大多数金融模型实现中不再是问题。

编译代码与解释代码

C++ 是一种编译语言,我们凡人输入文件的命令首先被翻译成计算机处理器可以理解的一组二进制指令或机器代码。 这与 Python、R 和 Matlab 等非类型化和解释性定量语言形成鲜明对比,在这些语言中,每行代码每次执行时都必须重新处理,从而减慢了大型应用程序的执行时间,尤其是在依赖严重的情况下 基于迭代(循环)语句。

这绝不是对这些语言的打击,因为它们的力量显而易见,可以快速实现和可视化金融、数据科学和生物科学等定量领域中出现的模型,而它们可用的数学和统计函数实际上是在这些领域中使用的。 通常已经用 C、C++ 或 Fortran 编译。 然而,财务程序员可能非常清楚模型需要几天时间才能在解释语言中运行的情况,而当用 C++ 重新实现时,运行时间可能会减少到几分钟或更短。

一种有效的方法是以互补的方式将解释型数学语言与 C++ 结合使用。 例如,计算密集型模型代码是在 C++ 库中编写,然后以交互方式调用或从 R 中的应用程序调用的情况。 C++ 有效地处理数字运算,而结果可以在 R 中强大的绘图和其他可视化工具中使用,而这些工具在 C++ 标准库中不可用。

另一个优点是模型代码编写一次并在 C++ 库中维护,该库可以跨许多不同的部门、部门甚至国际边界进行部署,并通过用不同前端语言编写的应用程序的接口进行调用,同时确保一致的数值结果 整个组织。 这对于监管合规目的特别有利。

流行的开源 C++ 集成包可用于 R 和 Python,分别是 Rcpp {5} 和 pybind11 {6}。 Matlab 还提供了 C++ 接口选项,尽管其附加功能可能需要支付不菲的许可费用。

C++ 的组成部分

高级别标准 C++ 版本由两个组件组成:语言功能和 C++ 标准库。 软件库本质上是一组函数和类,它们本身不能执行,但由应用程序或系统调用。 与前几十年更流行的独立应用程序相比,库开发——“开源和商业”——现在主导着现代 C++ 开发,我们稍后将讨论其中一些对计算工作有用的库开发。 最重要的 C++ 库是现代编译器附带的标准库。 标准 C++ 语言和标准库通常统称为标准。

C++ 语言特性

C++ 语言功能大部分与其他编程语言中常见的基本运算符和结构重叠,例如:

基本整数和浮点数值类型

条件分支:if / else 语句和 switch / case 语句

迭代构造:for 循环和 while 循环

数值类型的标准数学和逻辑运算符:加法、减法、乘法、除法、模数和不等式

C++语言特性支持四种主要编程范式:过程式编程、面向对象编程、泛型编程和函数式编程。 有助于提供每个功能的具体功能分别是:

独立功能

类和继承

模板

Lambda 表达式 (C++11 起)

最后,由于 C++ 是一种强类型语言,因此该语言提供了大量内置的数值和逻辑类型。 我们将主要使用的如下:

double(双精度)用于浮点值

int 表示正整数和负整数

非负整数无符号

bool 用于布尔表示( true 或 false )

每种数字类型的范围可能因平台而异,但在现代编译器上,此处提到的类型足以满足现代金融应用程序的需要。 您可以在 cppreference.com 上找到提供这些范围和详细信息的综合指南。

C++ 标准库

正如 Nicolai Josuttis 在他的不可缺少的文本《C++ 标准库 - 教程和参考》,第二版 {8} 中所描述的那样,C++ 标准库“使程序员能够使用通用组件和更高级别的抽象,而不会失去可移植性,而是 而不是必须从头开始开发所有代码。” 在最新的 C++20 版本中,用于金融建模的非常有用的库功能包括:

一维数组容器类,特别是动态调整大小的向量类

对这些数组容器进行操作的一组广泛的标准算法,例如排序、搜索以及将函数有效地应用于容器中的一系列元素

标准实值数学函数,例如平方根、指数和三角函数

复数和算术

从一组标准概率分布生成随机数

基于任务的并发性,可以提供并行运行的函数的返回值

智能指针可以消除与内存分配和管理相关的危险

用于存储和管理字符数据的字符串类

使用标准库组件需要程序员将它们显式导入到代码中,因为它们驻留在单独的库中而不是核心语言中。 这个想法类似于将 NumPy 数组导入到 Python 程序中或将外部函数包加载到 R 脚本中。 在 C++ 中,这是一个两步过程,首先加载包含我们希望使用的标准库函数和类的头文件,然后使用标准库命名空间名称 std 来确定这些函数的作用域,std 通常发音为“stood” C++ 开发人员。

作为第一个快速示例,创建一个 int 值向量和一个字符串对象,并将它们从 main() 函数内的简单可执行程序输出到控制台:

#include <vector>		// vector class#include <string>		// string class#include <iostream>		// coutint main(){	std::vector<int> x{ 1, 2, 3 };	std::string s{ "This is a vector: " };	std::cout << s << x[0] << ", " << x[1] << ", " << x[2] << "\n";}

请注意,标准库类 vector 和 string 以及控制台输出 cout 的作用域为标准库 std 命名空间。 如果您希望节省自己键入 std:: 的时间,则可以使用 using 语句,最好在单个函数中使用,尽管在某些有限情况下放置在文件顶部是可以接受的,例如编写一小组测试函数。

#include <vector>		// vector class#include <string>		// string class#include <iostream>		// coutint main(){	using std::vector, std::string, std::cout;	vector<int> x{ 1, 2, 3 };	string s{ "This is a vector: " };	cout << s << x[0] << ", " << x[1] << ", " << x[2] << "\n";}

每种情况的输出都不足为奇:

This is a vector: 1, 2, 3

using 语句仅需要一次,三个标准库组件位于同一行。 这是自 C++11 起的较新功能。 它也适用于作业,例如:

double a = 1.0, b = 2.0, c = 4.2;int i = 1, j = 2, k = 42;
笔记

对于本书中的示例,通常假设 using 语句已应用于常用的标准库类和函数,例如向量、字符串、cout 和 format。

笔记

将 std 命名空间导入到全局命名空间中

使用命名空间 std;

有时在代码中可以找到用 std 命名空间替换单个 using 语句或显式作用域; 然而,这不被认为是好的做法,因为它可能导致编译时的命名冲突。 更多详细信息可以在 ISO C++ 编码标准常见问题解答中找到,我应该在代码中使用 using 命名空间 std 吗? {9}。

笔记

在上面的代码示例中可以看到两个新功能(自 C++11 起):

1) 多个命令和声明可以放在一行中; 例如,

using std::vector, std::string;int i = 0, j = 1, k = 2;

2) 统一初始化(也称为支撑初始化),在上面的一些示例中使用,也是 C++11 中添加的功能:

vector<int> x{ 1, 2, 3 };string s{ "This is a vector: " };

一般来说,这是一个有用的功能,我们将立即讨论(请参阅一些新的 C++ 功能 — 下一节)。

笔记

cout 一般不在生产代码中使用。 我们将仅将其用作占位符,实际上结果更有可能传递到 GUI 或数据库界面或另一部分代码。

为了结束对标准库的介绍性讨论,除了作为 C++ 语言一部分讨论的内置类型之外,还需要注意的另一种类型是 std::size_t  — size 类型 — 。 相反,它包含在标准库中,并且“被定义为一个无符号整数,具有足够的字节来表示任何类型的大小”{10}。 然而,我们主要关心的是它是一个向量大小的返回类型,并且你需要将它用作上面的返回类型,并且可能在涉及向量或其他STL容器类型的索引循环中 :

#include <vector>#include <cstdlib>		// std::size_t. . .std::vector<int> v{1, 2, 3};std::size_t v_size = v.size();

对于三大编译器附带的标准库发行版 ——Visual Studio、Clang 和 gcc——  size_t 相当于 64 位 unsigned long 类型。 对于这本书,你真正需要了解的只是它的向量长度比我们需要的任何向量都要大得多。

C++11 以来的一些新特性

本节将介绍自 C++11(含)以来引入的一些有用的功能和语法,然后将以本书中将使用的一般风格指南作为结束语。 尽管 C++11 面世已经 10 多年了,但不幸的是,许多大学和量化金融课程仍然没有涉及它,更不用说 C++17 等后续版本了。

事实上,在使用功能丰富的语言(例如 C++)在金融系统中编写关键生产代码时,代码格式和变量命名的准则非常重要。 如果源代码是以一致、干净和可维护的状态编写的,那么错误、运行时错误和程序崩溃就更容易避免或解决。

auto关键字

C++11引入了auto关键字,可以自动推导变量或对象类型。 第一个简单的例子是:

auto k = 1;				// intauto x = 419.531;		// double

在这种情况下,k 被推导为 int 类型,x 被推导为 double 类型。

关于 auto 的使用存在不同的观点,但许多程序员仍然喜欢显式地声明基本类型,例如 int 和 double 以避免歧义。 这将是本书所遵循的风格。

当返回类型从上下文中相当明显时,auto 变得更有用。 正如我们将在第 3 章中看到的,使用标准库函数 make_unique<T>(.) 或 make_shared<T>(.) 创建唯一或共享指针:

auto call_payoff = std::make_unique<CallPayoff>(75.0);auto mkt_data = std::make_shared<LiveMktData>("CattleFutures");

在第一种情况下, make_unique<CallPayoff> 使得很明显正在创建一个指向 CallPayoff 对象的唯一指针(罢工为 75)。 在第二个中, make_shared<LiveMktData> 表示它正在创建一个指向 LiveMktData 对象的共享指针(提供牛期货价格)。 这些具有足够的表现力并且比以下更容易维护:

std::unique_ptr<CallPayoff> call_payoff = std::make_unique<CallPayoff>(75.0);std::shared_ptr<LiveMktData> mkt_data = std::make_shared<LiveMktData>("CattleFutures");

如果返回类型是长嵌套类模板类型(例如来自函数,如下所示),这也很方便:

std::map<std::string, std::complex<double>> map_of_complex_numbers(. . .){	. . .	return map_key_string_val_complex;

然后,而不是 C++11 之前的方式:

std::map<std::string, std::complex<double>> cauchys_revenge = map_of_complex_numbers(. . .);

我们可以更清晰地调用该函数并使用 auto 分配结果:

auto cauchys_revenge = map_of_complex_numbers(. . .);
基于范围的 for 循环

在 C++11 之前,迭代向量(或其他 STL 容器)需要使用索引作为计数器,最多可达其元素的数量。

vector<double> v;// Populate the vector v and then use below:for(unsigned i = 0; i < v.size(); ++i){	// Do something with v[i]}
vector<double> v{ 1.0, 2.0, 3.0, 4.0, 5.0 };for (unsigned i = 0; i < v.size(); ++i){	cout << v[i] << " ";}

或者,您可以使用基于迭代器的 for 循环:

for (auto iter = v.begin(); iter != v.end(); ++iter){	cout << *iter << " ";}

顺便说一句,请注意 auto 关键字意味着我们可以避免显式指定迭代器类型。

C++11 中引入的基于范围的 for 循环使其更加实用和优雅。 基于范围的 for 循环没有显式地使用向量索引,而是简单地说“对于 v 中的每个元素 x,用它做一些事情”,类似于使用 Python 或 R 所发现的情况:

for (double x : v){	cout << x << " ";}

基于范围的 for 循环也可用于对向量的元素应用操作。 作为一个简单的例子,计算元素的总和:

double sum = 0.0;for(double elem : v){	sum += elem;}

我们就完成了。 不用担心索引出错,代码更清楚地表达了它在做什么。 事实上,核心指南{11}告诉我们更喜欢对向量对象以及将在第 4 章中讨论的其他 STL 容器使用基于范围的 for 循环。

使用 using 关键字代替 typedef

在 C++11 之前,缓解神秘模板类型带来的痛苦的另一种方法是使用 typedef 。 回顾上面的复杂映射示例,我们可以定义一个别名,complex_map:

typedef std::map<std::string, std::complex<double>> complex_map;

从 C++11 开始,using 语句被扩展为执行相同的任务,但语法更自然:

using complex_map = std::map<std::string, std::complex<double>>;

此外,也许更重要的是,在函数和类模板中使用 typedef 需要一些额外且烦人的编码技巧,而 using 版本可以在非模板和模板情况下以相同的方式使用。

统一初始化

C++11 引入了统一初始化,也称为支撑初始化。 有几个用例,从初始化数字变量的简单情况开始:

int i{ 106 };

这并不是很有趣,因为它只是替换了 int i = 106; 。 但是,如果我们有以下情况怎么办?

double x = 92.09;int k = x;		// Compiles with warning

这将编译,尽管有一些警告,表明 x 的小数部分将被截断,仅留下 k 保留 92。 通过统一初始化,编译器将发出缩小转换错误并停止构建,从而防止运行时出现意外行为。 这是一件好事,因为在编译时捕获错误比在运行时捕获错误更好。

int n{ x };		// Compiler ERROR: narrowing conversion

统一初始化可以使您避免在对象初始化期间缩小转换的问题。 我们将在本书后面讨论这一点,但现在,考虑以下简单的类:

class SimpleClass{public:	SimpleClass(int k) :k_{ k } {}	// Braced initialization in constructor also	SimpleClass() = default;		// C++11 form of default constructor									// (to be discussed in more detail in Ch XX)private:	int k_;};

如果我们有

double x = 2.58;		// Assigned or input somewhere. . .SimpleClass sc_01(x);

这将成功编译,但再次警告 x 的 double 值正在转换为 int ,这将截断十进制值。 但是,通过使用统一初始化:

SimpleClass sc_02{ x };		// Compiler ERROR

编译器将因错误而停止,再次防止运行时出现意外行为。

使用正确的类型将成功生成该类的实例:

int k = 319;		// Assigned or input somewhere. . .SimpleClass sc_03{ k };

请注意,还有一个默认构造函数。 在 C++11 之前,调用默认构造函数不带圆括号:

SimpleClass sc_04;

如果您错误地输入了以下内容,有时可能会出现问题:

SimpleClass sc_04();

这可以编译,但编译器会将其视为 SimpleClass 函数的声明。 通过统一初始化,两个构造函数都使用大括号,因此我们现在在使用参数的情况和默认构造函数之间具有一致性:

SimpleClass sc_05{};

关于统一初始化的另一点是,它可以通过与 auto 相同的类型推导来减少冗长。 例如,假设我们有一个返回 SimpleClass 对象的函数 some_function(.)。 由于编译器知道返回类型,因此它需要的只是构造函数参数,用大括号指示,而不是再次键入 SimpleClass。 返回对象可以简单地使用统一初始化来构造。

SimpleClass some_function(int m){	if (m >= 0)	{		return {2 * m};		// Returns SimpleClass{ 2 * m }	}	else	{		return {-2 * m};	// Returns SimpleClass{ -2 * m }	}}

这也可以在填充向量或其他容器时使用:

vector<SimpleClass> v{ {1}, {2}, {3} };

SimpleClass 对象的向量可以通过 v 的每个 SimpleClass 元素的统一初始化来初始化。

在上面的示例中,SimpleClass 构造函数接受单个参数,但这可以扩展到构造函数具有多个参数的情况。 这将在本书后面的示例中看到。

笔记

统一初始化的另一种等效形式是在变量名称和左大括号之间放置等号,例如:

int i = { 106 };vector<SimpleClass> v = { {1}, {2}, {3} };

在本书中,将使用上面不带等号的原始形式。

笔记

在向量的情况下,统一初始化规则有一个例外。 仅根据上面的讨论,人们可能会期望以下内容创建一个由两个整数组成的向量 u:

vector<int> u{ 2 };

然而,正如我们在当前讨论之前所看到的,这实际上会用一个 int 元素 2 初始化一个向量。要创建一个包含两个元素的向量 v,您仍然需要使用旧的圆括号形式来指示 2 是构造函数 参数而不是数据值:

vector<int> v(2);
格式化输出

C++20 中值得一提的新功能是可以在 cout 语句中流式传输的新格式命令。 它类似于其他语言中的输出命令,特别是 C# 中的 Console.WriteLine(.)。 在涉及基本类型或字符串的输出时,可以更轻松地添加描述性文本。

例如,假设我们有两个变量 u 和 v,它们已被分配了一些值:

double u = 1.5;double v = 4.2;

如果我们想用变量名标签输出这些值,我们可以这样写:

cout << "u = " << u << ", v = " << v << "\n';

然而,将 V 形链连接在一起可能会变得很烦人。 相反,我们现在可以使用:

#include <format>. . .cout << std::format("u = {0}, v = {1}", u, v) << "\n";

这表示将 u 放在 "u = " 之后的第一个(0 索引)位置,然后将 v 放在以下位置:

u = 1.5, v = 4.2

但是,当顺序是从左到右时,可以删除索引值:

cout << std::format("u = {}, v = {}", u, v) << endl;

然而,在某些情况下,需要索引值; 例如:

	double w = std::sin(u) + v;	cout << "\n"		<< std::format("u = {0}, v = {1}, sin({0}) + {1} = {2}", u, v, w) << "\n";

重申一下,生产代码中的控制台输出很少见(如果有的话),但在单独学习 C++ 时可以作为一种有用的工具。 因此,它将在整本书中用于演示输出结果。

类模板自动推导 (CTAD)

C++17引入了类模板自动推导,简称CTAD。 与 auto 类似,它将根据初始化的数据推断出模板参数的类型。 所以,代替前面的例子

std::vector<int> x{ 1, 2, 3 };

我们可以直接删除 int 模板参数来获得相同的结果:

std::vector x{ 1, 2, 3 };

上面的示例也很简单,仅使用硬编码值,但 CTAD 在更现实的情况下可以减轻符号并使代码更具可读性。 我们将在本书后面看到这样的例子,特别是在第七章中,当使用多维数组视图 std::mdspan 时,它在 C++23 中发布。

枚举常量和作用域枚举

在 C++11 之前,枚举常量(通常简称为枚举)是一种很好的方法,可以通过将整数代码表示为命名常量,让我们这些普通人更清楚地理解整数代码。 机器处理整数也比传递占用更多内存的笨重 std::string 对象要高效得多。 最后,可以避免由引号字符和杂散字符串中的拼写错误引起的错误。

C++11 标准通过作用域枚举(使用 enum class )进一步改进了这一点。 这些消除了使用常规枚举常量时重叠整数值可能出现的歧义,同时保留了优点。

下面介绍了选择更现代的作用域枚举而不是基于整数的枚举的动机。

枚举常量

首先举个例子,我们可以创建一个名为 OptionType 的枚举,它代表简单交易系统中允许的期权交易类型,例如欧洲、美国、百慕大和亚洲。 声明了枚举名称(OptionType); 然后,在大括号内定义特定的常量值,并用逗号分隔。 默认情况下,将为每个值分配一个从零开始并递增一的整数值(与 C++ 中从零开始的索引一致)。 右大括号后面必须跟一个分号。 在代码中,我们会这样写:

enum OptionType{    European,     	// default integer value = 0    American,     	// default integer value = 1    Bermudan,     	// default integer value = 2    Asian	      	// default integer value = 3};

只是为了验证每个相应的整数值,将其输出到屏幕:

cout << " European = " << European << endl;cout << " American = " << American << endl;cout << " Bermudan = " << Bermudan << endl;cout << " Asian = " << Asian << endl;cout << endl;

检查输出,我们得到:

European = 0American = 1Bermudan = 2Asian = 3
与枚举的潜在冲突

正如一开始所讨论的,对于任何枚举类型,默认整数分配从零开始,然后为每个值加一。 因此,来自两种不同类型的两个枚举常量可能在数值上相等。 例如,假设我们定义两种不同的枚举类型,称为 Football 和 Baseball ,代表每种运动中的防守位置。 默认情况下,棒球投手位置从 0 开始,列表中的每个位置都加 1。 (美式)橄榄球的位置也是如此,从防守截锋开始。 注释中提供了整数常量。

enum Baseball{	Pitcher,		// 0	Catcher,		// 1	First_Baseman,	// 2	Second_Baseman,	// 3	Third_Baseman,	// 4	Shortstop,		// 5	Left_Field,    	// 6	Center_Field,	// 7	Right_Field		// 8};enum Football{	Defensive_Tackle,	// 0	Edge_Rusher,		// 1	Defensive_End,		// 2	Linebacker,			// 3	Cornerback,			// 4	Strong_Safety,		// 5	Weak_Safety			// 6};

然后,我们可以比较 Defective_End 和 First_Baseman :

if (Defensive_End == First_Baseman){	cout << " Defensive_End == First_Baseman is true" << endl;}else{	cout << " Defensive_End != First_Baseman is true" << endl;}

我们的结果将是无稽之谈:

Defensive_End == First_Baseman is true

这是因为两个位置都映射到整数值 2。一种快速修复方法(在 C++11 之前经常采用)是重新索引每组枚举; 例如,

enum Baseball{	Pitcher = 100,	Catcher,		// 101	First_Baseman,	// 102	. . .};enum Football{	Defensive_Tackle = 200,	Edge_Rusher,	// 201	Defensive_End,	// 202	. . .};

现在,如果我们比较 Defective_End 和 First_Baseman ,它们将不再相等,因为 202 $\neq$ 102。不过,在大型代码库中可能有数百个枚举定义,因此重叠也不是不可能的 滑入并导致错误。 C++11 中引入的枚举类消除了这种风险。

具有枚举类的作用域枚举

在更现代的 C++ 中,更强大的方法完全消除了整数值。 枚举的其他好处仍然存在,例如避免神秘的原始数字代码或依赖字符串对象,但是通过使用所谓的作用域枚举来避免像上面所示的数字冲突,该枚举是通过枚举类完成的,在 C+ 中添加 +11。 例如,我们可以定义债券和期货合约类别,如下所示:

enum class Bond{	Government,	Corporate,	Municipal,	Convertible};enum class Futures_Contract{	Gold,	Silver,	Oil,	Natural_Gas,	Wheat,	Corn};enum class Options_Contract{    European,    American,    Bermudan,    Asian};

请注意,我们不再需要像使用常规枚举那样手动设置整数值以避免冲突。

尝试比较两个不同枚举类的成员——例如 Bond 和 Futures_Contract 头寸,现在将导致编译器错误。 例如,以下内容甚至无法编译(使用默认的 == 运算符):

if(Bond::Corporate == Futures_Contract::Silver){	// . . .}

这对我们有利,因为在编译时捕获错误比在运行时捕获错误要好得多。 核心准则{12}现在认为我们应该更喜欢使用枚举类而不是枚举常量,“以尽量减少意外情况”。

如果需要,仍然可以将作用域枚举转换为整数索引值。 例如,Bond::Corporate 和 FuturesContract::Silver 都是其各自枚举类中的第二个成员,因此默认情况下,每个成员都可以转换为值 1 ,即使它们不具有可比性。

cout << format("Corp Bond index: {}", static_cast<int>(Bond::Corporate)) <<  "\n";cout << format("Silver Futures index: {}", static_cast<int>(Futures_Contract::Silver)) << "\n";

输出将是

11

还可以分配特定的索引值:

enum class Option_Type{	European	= 100,	American	= 102,	Bermudan	= 104,	Asian		= 106};

但同样,不存在与其他枚举类成员等价的风险,因为两个类类型不可比较并被编译器阻止。 然而,在某些情况下,使用数字表示会很方便,这将在后面的章节中看到。

最后,一般来说,枚举,特别是枚举类,是 switch / case 语句的自然补充。

void switch_statement_enum(OptionType ot){	switch (ot)	{	case OptionType::European:		std::cout << "European: Use Black-Scholes" << "\n";		break;	case OptionType::American:    // AMERICAN case matches....		cout << "American: Use a lattice model" << "\n";		break;	case OptionType::Bermudan:		cout << "Bermudan: Use the Longstaff-Schwartz LSMC model" << "\n";		break;	case OptionType::Asian:		cout << "Asian: Calculate average of the returns time series" << "\n";		break;	default:		cout << "The SEC might want to talk with you" << "\n";		break;	}}
Lambda 表达式

lambda 表达式通常称为匿名函数对象,该术语表面上是根据其 2006 年的原始设计提案创造的{13}。 在白话中也称为 lambda 函数,或者只是简单的 lambda,它可以在另一个函数体内动态定义函子。 此外,就像我们已经讨论过的类仿函数一样,lambda 可以作为参数传递到另一个函数中。 最后一个属性使得它们在 STL 算法中使用起来如此强大,这将在第四章中介绍。

Lambda 表达式是现代 C++ 库中受欢迎的补充。 首次在 C++11 中引入,随后的每个版本中都进行了各种增强,包括最新的 C++20 标准。 有关这些改进的演变的精彩总结可以在 {14}(Jonathan Boccara)中找到。

首先,使用一个 lambda 表达式来打印出古老的“Hello World!” 可以在封闭函数内写如下:

void worlds_simplest_function(){	auto f = []	{		std::cout << "Hello World!" << "\n";	};	f();}

lambda 还可以通过使用可选的圆括号来接收函数参数,就像常规的 C++ 函数一样:

auto g = [](double x, double y){	return x + y;};double z = g(9.2, 2.6);	// z = 11.8

此阶段需要注意三点:

首先,lambda 的返回类型为 auto ,但可以选择指示显式返回类型是一个选项,如下所示:

auto g = [](double x, double y) -> double{	return x + y;};

这也适用于返回类的对象:

auto some_lambda = [](int n) -> SimpleClass{	return { n };		// Braced initialization of a SimpleClass type};

其次,请确保在 lambda 块末尾放置一个分号。 当像第一个示例一样放在一行上时,这有点明显,因为它看起来就像任何其他单行 C++ 语句一样。 但是,如果您的 lambda 实现跨越多行,则很容易忽略它,在这种情况下您的代码将无法编译。

第三,在没有函数参数的情况下,例如在第一个示例中,圆括号是可选的,但为了定义 lambda,方括号是必需的。 上面示例中的方括号是空的,但通常它们提供 lambda 表达式的捕获。

捕获 lambda 表达式的作用正如其所言,即捕获(非静态)外部变量,包括对象,允许它们在 lambda 体内使用。 捕获数据可以通过值或非常量引用来获取,后一种选择可能会导致修改。 在形成示例之前,假设向 SimpleClass 添加了一个 mutator 函数:

class SimpleClass{	public:		. . .		void reset_val(int k)		{			k_ = k;		}	. . .

然后,在下面的示例中,构造了一个 SimpleClass 对象 sc,然后允许通过引用在 lambda 中捕获该对象,同时初始化 alpha 变量。

void lambda_capture_reference(){	SimpleClass sc{ 7 };		// k_ = 7	int alpha = 1;	auto change_value = [&sc, alpha](int n) -> void	{		sc.reset_val(n + alpha);	};	change_value(2);		// Modified inside the lambda.	// Now k_ =  n + alpha = 2 + 1 = 3}

当调用 lambda 函数 change_value 时,它会同时通过引用捕获 sc 和通过值捕获 alpha。 sc 被修改为其新状态,由于它是通过引用捕获的,因此该状态反映在外部。 通过值捕获的 alpha 只在 lambda 内部修改。 捕获可以包含任意数量的变量,但您需要确保不要同时通过值和引用来指定单个变量; 例如,把

[sc, &sc, alpha]

上面也会导致编译器错误。

笔记

使用通配符 [=] 或 [&] 作为 lambda 捕获将允许分别通过值或引用捕获任何前面的外部变量或对象。 然而,C++ 开发人员对于这是否是好的做法存在一些争议。 例如,斯科特·迈耶斯 (Scott Meyers) 指出(直接引用斜体字){15}:

使用 [&] 可能会导致悬空引用或意外行为

使用 [=] 可能会因意外复制指针(包括智能指针)而导致问题

避免这些默认值还可以增加在编译时而不是在运行时发现问题的可能性

C++ 中的数学运算符、函数和常量

数值类型的标准数学运算符可作为 C++ 中的语言功能使用,下面将进行全面的讨论。 然而,常见的数学函数——比如余弦、指数等——加上一组更新的特殊函数,是在 C++ 标准库中提供的,而不是在核心语言中提供的。 下面也介绍了这些内容。

标准算术运算符

C++ 中分别使用运算符 + 、 - 、 * 和 / 提供数值类型的加法、减法、乘法和除法,如其他编程语言中常见的那样。 此外,还为整数类型( int 、 unsigned 、 long 等)定义了模运算符 % 。 示例如下:

// integers:int i = 8;int j = 5;int k = i + 7;int v = j - 3;int u = i % j;// double precision:double x1 = 3.06;double x2 = 8.74;double x3 = 0.52;double y = x1 + x2;double z = x2 * x3;

算术运算符的顺序和优先级与大多数其他编程语言以及中学数学中的相同,即:

顺序从左到右:

i + j - v

使用上述整数值将得到 8 + 5 - 2 = 11

乘法、除法和模数优先于加法和减法,例如:

x1 + y / z

使用上述双精度值将导致

使用圆括号更改优先级,例如:

(x1 + y) / z

会产生

还包括复合赋值运算符,例如

x1 = x1 + x2;

可以用附加赋值替换:

x1 += x2;

其余运算符 - 、 * 、 / 和 % 也有各自的版本。

标准库中的数学函数

在其他语言中发现的许多常见数学函数在 C++ 中具有相同或相似的语法。 计算应用中常用的函数包括下表中列出的函数,其中 x 、 y 和 z 假定为双精度变量:

表 1-1。 标准库数学函数

C++

Description

cos(x)

cosine of $x$

sin(x)

sine of $x$

tan(x)

tangent of $x$

exp(x)

exponential function $e^x$

log(x)

natural logarithm $ln(x)$

sqrt(x)

square root of $x$

cbrt(x)

cube root of $x$

pow(x, y)

$x$ raised to the power of $y$

hypot(x, y)

computes $\sqrt{x2+y2}$

hypot(x, y, z)

computes $\sqrt{x2+y2+z^2}$ (since C++17)

cppreference.com 上提供了常见数学函数 {16} 的完整列表,这是任何 C++ 开发人员的重要资源。 Josuttis,第 17.3 节((前述){8})也提供了非常丰富的信息。

由于这些包含在标准库中而不是作为语言功能,因此应始终包含 cmath 头文件,其函数范围由 std:: 前缀限定,即

#include <cmath>      // Put this at top of the file.double trig_fcn(double theta, double phi){  return = std::sin(theta) + std::cos(phi);}// Or, alternativelydouble zero_coupon_bond(double face_value, double int_rate, double year_fraction){	using std::exp;    return face_value * exp(-int_rate * year_fraction);}

关于 <cmath> 函数需要注意的两点如下。 首先,C++ 中没有幂运算符。 与其他语言不同,指数通常由 ^ 或 \** 运算符表示,这并不作为 C++ 语言功能存在(运算符 ^ 确实存在,但不用于此目的)。 相反,需要调用 <cmath> 中的标准库 std::pow(.) 函数。 然而,在计算多项式时,根据霍纳方法应用因式分解并减少乘法运算的数量可能会更有效(Stepanov){17}。 例如,如果我们希望实现一个功能

最好用 C++ 将其编写为

double f(double x){  return x * (x * (x * (8.0 * x + 7.0) + 4.0 * x) - 10.0) - 6.0;}

而不是

double f(double x){  return 8.0 * std::pow(x, 4) + 7.0 * std::pow(x, 3) +    4.0 * std::pow(x, 2) + 10.0 * x - 6.0;}

性能结果可能取决于所使用的编译器以及使用不同的编译器优化,但使用霍纳方法几乎肯定不会更糟。

对于非整数指数的情况,说

那么没有可用的替代方法来使用 std::pow(.) :

double g(double x, double y){  return std::pow(x, -1.368 * x) + 4.19 * y;}

其次,您也许可以在没有 #include <cmath> 的情况下使用其中一些函数。 不幸的是,由于 C++ 与 C 的长期联系,这是 C++ 的怪癖之一。 然而,这个故事的寓意非常简单:为了保持 C++ 代码 ISO 兼容,从而帮助确保不同编译器和操作系统平台之间的兼容性,应该始终放置 #include <cmath> ,并使用 std 限定数学函数的范围: : 。 请勿包含 math.h C 标头。

一个典型的例子是绝对值函数。 在某些 C++ 编译器中,默认(全局命名空间)abs(.) 函数可能是 math.h 的继承,仅针对整数类型实现。 为了计算 math.h 中浮点数的绝对值,需要使用 fabs(.) 函数。 然而, std::abs(.) 对于整数和浮点(例如 double )参数都是重载的,应该是首选。

<cmath> 函数也与 C 对应函数分开发展,包括针对 C++ 的优化。 这是更喜欢标准库版本的又一个原因。 根据 GNU C++ 库手册 {18},

该标准指定,如果包含 C 样式标头(在本例中为 math.h),则符号将在全局命名空间中可用,也可能在命名空间 std 中可用,但这不再是硬性要求。 另一方面,包含 C++ 样式标头 ( <cmath> ) 可保证在命名空间 std 中找到实体,并且可能在全局命名空间 {18} 中找到实体(斜体直接引用,作者用粗体强调)。

所以,长话短说:使用 #include <cmath> 和范围数学函数与 std:: 。

数学特殊函数

这是一组特殊函数,例如勒让德多项式、埃尔米特多项式、贝塞尔函数和指数积分。 这些函数经常在物理学中使用,但由于定量金融与物理学世界有着密切的历史联系,因此您可能会在高级衍生品建模中遇到它们。 在朗斯塔夫和施瓦茨关于期权定价的最著名和被引用的论文之一《通过模拟评估美式期权:简单的最小二乘法》{19}中,勒让德多项式和埃尔米特多项式都可以用作基础(等等) 对于底层模型。

进一步的讨论超出了本书的范围,但有关特殊数学函数的更多详细信息,{20} (Josuttis: {cpp17}) 和 {21} (cppreference.com) 是很好的资源。

标准库数学常数

C++20 标准库的一个方便补充是一组常用的数学常量,例如 $\pi$、$e$、$\sqrt{2}$ 等的值。其中一些很方便 量化金融的情况如下表所示。

表 1-2。 标准库数学函数

C++ 常量

e

pi

inv_pi

inv_sqrt_pi

sqrt2

定义

$e$

$\pi$

$\frac{1}{\pi}$

$\frac{1}{\sqrt{\pi}}$

$\sqrt{2}$

要使用这些常量,必须首先将数字标头包含在标准库中。 在撰写本文时,每个名称空间都必须具有 std::numbers 命名空间。 例如,要实现功能

我们可以写

#include <cmath>#include <numbers>. . .double math_constant_fcn(double x, double y){	double math_inv_sqrt_two_pi =		std::numbers::inv_sqrtpi / std::numbers::sqrt2;	return math_inv_sqrt_two_pi*(std::sin(std::numbers::pi * x) +		std::cos(std::numbers::inv_pi*y));}

这样,例如,每当在计算中使用 $\pi$ 时,其值在整个程序中都将保持一致,而不是将其留给项目中的不同程序员,他们可能会使用不同精度的近似值,从而导致可能的不一致 数值结果。

此外,$\sqrt{2}$ 的值在数学计算中经常出现,不必用

std::sqrt(2.0)

每次需要的时候。 常数

std::numbers::sqrt2

本身保持双精度近似值。 虽然就一次性性能而言可能影响不大,但在计算密集型代码中重复调用 std::sqrt 函数数百万次可能会产生一些影响。

笔记

虽然此时不必了解,但值得一提的是,这些常量是在编译时固定的,而不是在每次调用运行时使用名为 constexpr 的 C++11 名称进行计算。

作为结束语,有点奇怪的是,C++20 中提供的数学常量集包括值 $\frac{1}{\sqrt{3}}$,但不包括 $\frac{1}{\sqrt {2}}$ 或 $\frac{1}{\sqrt{2 \pi}}$,尽管后两者是更常见的数学和统计函数,包括金融中使用的函数。 然而,后两者包含在 Boost Math Toolkit 库中,将在第八章中介绍。

命名约定

到目前为止,我们只是从示例开始,没有讨论这个主题。 这些示例一开始相当简单,但是随着代码的涉及越来越多,最好退一步并以有关命名约定和编码风格以及本书将使用的风格指南的简短讨论作为结束。 更重要的是,在现实生活中的金融 C++ 开发工作中,您使用和编写的代码的复杂性将发生质的飞跃,因此这些问题成为必然。

现在,首先回顾一下,变量、函数和类名称可以是字母和数字的任意连续组合,但需满足以下条件:

名称必须以字母或下划线开头; 不允许使用前导数字(可以使用非前导数字)。

除下划线字符外,不允许使用特殊字符,例如@、=、$等。

不允许有空格。 名称必须是连续的。

语言关键字是保留关键字,不允许同时作为名称,例如 double 、 if 、 while 等。保留关键字的完整列表可以在 {22} (cppreference.com) 中找到。

笔记

关于第一个要点,从技术上讲,以下划线开头的名称是合法的; 然而,实际上,它们通常保留给编译器使用,因此通常不鼓励使用。 某些编码风格确实对私有类成员使用尾随下划线,本书就是这种情况。

单字母变量和函数名称适合简单的示例和简单的数学函数。 然而,对于定量金融模型的实现、交易和风险系统等,通常最好传递具有更具描述性名称的函数参数。 函数和类名称也应该提供一些它们的作用的指示。 此外,作为一个团体或公司,决定一组命名和样式规则非常重要,以增强代码的可维护性并降低代码中出现错误的风险。

多年来,几种命名风格很常见,即

驼峰小写; 例如, optionDelta 、riskFreeRate 、 effectiveFrontier :第一个单词的字母小写,后面的单词大写

上骆驼案,又名帕斯卡案; 例如,OptionDelta、RiskFreeRate、EfficientFrontier:每个单词的字母均为大写

蛇箱; 例如, option_delta 、risk_free_rate 、 effective_frontier :每个单词以小写字母开头,用下划线字符分隔

Lower Camel 和 Snake 形式是 C++ 函数和变量名称中常见的典型情况,类名称通常采用 Upper Camel 形式。 Microsoft {23} 更喜欢 Lower Camel 大小写,而 C++ 标准则使用 Snake 大小写,Google 的 C++ 样式指南 {24} 也是如此。 用户应该采用他或她正在工作的团队的风格,并且通常对这两种风格都感到满意。 在本书中,我们将使用蛇形命名法作为函数和变量名,使用大驼峰命名法作为类名。 此外,私有成员变量和成员函数将使用尾随下划线,例如:

class Blah{	public:		Blah(double x, . . .);		void calc_blah(double y) const;		. . .	private:		double x_;		double do_something_();};

在将单个字符用于整数计数变量的情况下,仍然通常使用字母 i 到 n 的 Fortran 约定,尽管这不是必需的。 我们大部分也会采用这种做法。

有关如何不编写代码的示例,文章“如何编写不可维护的代码确保终身工作;-){25}”提供了一个幽默但并非无关紧要的观点。 引用荷马·辛普森的话,“这很有趣,因为这是真的”(d'oh){26}。

概括

C++大致分为两个部分,即语言特性和标准库。 总的来说,它们通常被称为标准,并且主要编译器供应商——尤其是“三巨头”,Microsoft Visual Studio、LLVM Clang 和 GNU gcc 将在其编译器版本中包含各自的标准库发行版。 标准库的实施主要由各个供应商决定,只要他们符合 ISO 标准要求即可。

C++ 在金融软件开发中流行的兴衰背后有一些历史。 然而,在与 Java 和 C# 竞争之后,随着 C++11 在该领域的发布,C++ 开始经历一些复兴。 随着后续每三年发布一次——“目前 C++20 和 C++23 即将发布”——新功能和廉价的抽象已经取代了许多曾经必须从头开始完成的编码,从而在有效使用时产生更清晰的代码 。 其中一些,例如 auto 关键字、基于范围的 for 循环、作用域枚举、lambda 表达式和数学常量,我们已在上面讨论过。 应用于财务软件的更多具体示例将在后续章节中介绍。

参考

{1}ISO C++ 委员会

{2} 今日 C++:野兽回来了,作者:Jon Kalb 和 Gašper Ažman

{3}ISO C++ 核心指南

{4} ISO C++ 编码标准

{5} Hanson,将 R 包中的 C++ 代码与 Rcpp 集成

{6} pybind11。 注意:还可以在这里找到 pybind11 的工作示例(Steven Zhang)

{7} cppreference.com[C++ 数值类型,cppreference.com

{8}Nicolai Josuttis,C++ 标准库 - 教程和参考,第二版。 也可在 O’Reilly Learning 上找到

{9} 我应该在代码中使用 using 命名空间 std 吗?,ISO C++ 编码标准

{10}Christopher Di Bella,为什么某些 C++ 程序使用 size_t?

{11} ISO 核心指南,首选基于范围的 for 循环

{12} ISO 核心指南,枚举类

{13}Stroustrup 等人,Lambda 表达式提案 N1968。

{14} Jonathan Boccara,C++14、C++17 和 C++20 中 Lambda 的演变

{15}Scott Meyers,《Effective Modern C++》,O’Reilly,第 31 条(第 217 页和第 220 页)

{16} 常见数学函数,cppreference.com

{17}Stepanov 和 Rose,从数学到通用编程(霍纳方法)

{18} GNU C++ 库手册,_ C 标头和命名空间 std

{19}Longstaff 和 Schwartz,通过模拟评估美国期权:一种简单的最小二乘法

{20} Josuttis,C++17 完整指南 (28.3.3)

{21} cppreference.com,特殊数学函数 _

{22} cppreference.com,保留语言关键字

{23} Microsoft 编码风格约定

{24}Google C++ 风格指南

{25} 如何编写无法维护的代码 确保终身工作;-)

{26}荷马·辛普森,《荷马对丽莎和第八诫》,《辛普森一家》第 2 季,第 13 集(福克斯)

标签: #c语言界面库