面向 C 程序员的现代 C++

2021-07-24 07:20:30

注意:如果你喜欢这些东西,来 PowerDNS 和我一起工作 - 有抱负的 C++ 程序员欢迎!欢迎阅读面向 C 程序员的现代 C++ 第 1 部分,请参阅本系列的目标和背景介绍。在这一部分中,我们从 C++ 特性开始,您可以使用这些特性“逐行”添加代码,而无需立即使用“C++ 编程语言”的全部 1400 页。 C 和 C++ 实际上是非常接近的亲戚,以至于许多编译器都为这两种语言提供了统一的基础架构。换句话说,您的 C 代码已经通过与 C++ 共享的代码路径(并且可能是用 C++ 编写的)。事实上,当 TrivialC 程序用 g++ 编译为 C++ 时,会出现大小相同的二进制文件。我们钟爱的 The C ProgrammingLanguage 中的所有示例程序都编译为有效的 C++。有趣的是,1988 年版的 K&R notes Bjarne Stroustrup 的 C++“翻译器”被广泛用于本地测试。这种关系更进一步——整个 C 库都包含在 C++ 的“byreference”中,并且 C++ 知道如何调用所有 C 代码。相反,完全有可能从 C 调用 C++ 函数。C++ 被明确设计为不存在与 C 相比不可避免的开销。引用 ISO C++ 网站的话说:零开销原则是 C++ 设计的指导原则。它指出:你不使用的东西,你不付钱(在时间或空间上)并且进一步:你使用的东西,你不能更好地编写代码。

换句话说,不应向 C++ 添加任何会使任何现有代码(不使用新特性)变大或变慢的特性,也不应添加任何使编译器生成的代码不如程序员在不使用新特性的情况下创建的代码的特性。功能。这些都是很大的主张,它们确实需要一些证据。为了在 2018 年实现这一目标,我们必须小心。许多代码使用异常,并且这些都带有一些开销。但是,也可以声明我们的全部或部分代码是无异常的,这会导致编译器删除该基础结构。但是,这里有实际的证据。使用 C qsort() 函数对 1 亿个整数进行排序,使用 C++ 中的 std::sort() 并使用 C++-2017 并行排序,我们得到以下计时:C qsort(): 13.4 秒 (13.4 CPU)C++ std:: sort():8.0 秒(8.0 CPU)C++ 并行排序:1.7 秒(11.8 秒的 CPU 时间)这是什么魔法? C++ 版本比 C 快 40%?这怎么可能? int cmp(const void* a, const void* b){ if(*(int*)a < *(int*)b) return -1;否则 if(*(int*)a > *(int*)b) 返回 1;否则返回0; }int main(int argc, char**argv){ auto lim = atoi(argv[1]); std::vector<int> vec; vec.reserve(lim); while(lim--) vec.push_back(random()); if(*argv[2]=='q') qsort(&vec[0], vec.size(), sizeof(int), cmp);否则 if(*argv[2]=='p') std::sort(std::execution::par, vec.begin(), vec.end()); else if(*argv[2]=='s') std::sort(vec.begin(), vec.end());} 值得研究一下。 cmp() 函数用于 qsort(),并定义排序顺序。

Main 和 C 中一样是 main,但是我们看到了第一个奇怪的东西:auto。我们稍后会介绍这个,但是 auto 几乎总是做你认为它会做的事情:计算所需的类型并使用它。接下来的两行定义了一个包含整数的向量,并在其中为我们想要的条目数量保留足够的空间。这是一个可选的优化。 while 循环然后用“随机”数字填充向量。接下来……神奇的事情发生了。我们调用 C qsort() 函数,对包含我们的数字的 C++ 向量进行操作。这怎么可能?事实证明 std::vector 被明确设计为可与原始指针操作互操作。它旨在能够传递给 C 库或系统调用。它将数据存储在可以随意更改的连续内存块中。接下来的 4 行使用 C++ 排序函数。在某些版本的 G++ 上,您可能需要这种(非标准)语法来获得相同的结果:__gnu_parallel::sort(vec.begin(), vec.end())。 qsort() 是一个接受比较回调的库函数。因此,编译器(及其优化器)无法将 qsort() 过程视为一个整体。此外,还有函数调用开销。同时,C++ std::sort 版本实际上是一个“模板”,它能够内联比较谓词,对于 int,它默认为 <operator。为了确保我们是公平的,因为 qsort() 使用的是自定义比较器,而我们的 std::sort 不是,我们可以使用:

std::sort(vec.begin(), vec.end(), [](const auto& a, const auto& b) { return a < b; } );执行时,这仍然需要相同的时间。要逆序排序,我们可以将 a < b 更改为 b < a。但是这个神奇的语法是什么?这是一个 C++ lambda 表达式,一种定义内联函数的方法。这可以用于很多事情,并且以这种方式定义排序操作​​是非常惯用的。最后,C++ 2017 附带了许多核心算法的并行版本,对于我们的案例,似乎并行排序确实在我的 8 超核机器上提供了 4.7 倍的加速。可能很难相信,但在 C++ 最初开发的大部分时间里,它并没有字符串类。写这样的课有点像成人礼,每个人都自己做。这背后的部分原因是长期尝试制作一个适合所有人的课程。 auto pos = fname.find('/');if(pos != string::npos) cout << "First / is at " << pos << "\n";pos = fname.find("host" );if(pos != string::npos) cout << "Found host at " << pos << "\n";std::string newname = fname;newname += ".backup";unlink(newname. c_str()); std::string 使用 [] 运算符提供对其字符的不安全和未经检查的访问,因此 newname[0] == '/',但明智的人使用 newname.at(0) 执行边界检查。 2011 年之后的 std::string 设计非常有趣。基本字符串实现的存储可能如下所示:

在现代系统上,这是 24 字节的数据。容量字段用于存储已分配多少内存,以便 mystring 知道何时需要重新分配。每次将字符添加到 astring 时都不重新分配是一个很大的胜利。然而,我们存储在字符串中的东西经常比 24 字节短很多。为此,现代 C++ std::string 实现实现了小字符串优化,这允许它们在自己的存储中存储 16 甚至 21 个字节的字符,而无需使用 malloc(),这是一个加速。防止对 malloc() 进行不必要调用的另一个好处是,字符串数组现在存储在连续内存中,这对于内存缓存命中率非常有用,这通常会提供整个加速因素。经过多年的设计,std::string 可能不是每个人的全部,但与“零开销”原则一致,它胜过您可以快速手写的内容。在本系列的第 1 部分中,我希望向您展示一些您可以立即开始使用的 C++ 的有趣部分 - 为您提供许多新功能,而无需立即用复杂的东西填充您的代码。如果您有任何喜欢的东西想看到讨论或问题,请联系我 @PowerDNS_Bert 或 [email protected] 注意:如果您喜欢这些东西,请与我一起在 PowerDNS 工作 - 有抱负的 C++ 程序员欢迎!