面向C程序员的现代C++:第一部分
注意 本人的博客都迁移到本人自己搭建的博客地址,通过此处可查看。
对于一个程序员来说2018年是一个激动人心的时刻,因为有那么多优秀的“全服务”编程语言可供选择:C、C++ 2017、Go、Python、Rust,还有人人皆知的Swift。编程语言是一种复杂的东西——即使是最简单的语言也有运行在(或可能运行)数百页中的规范,一旦你包含了所有内容,任何严肃的语言都不可能记录在1000页之内。
随之而来的事实是,每一种编程语言都有好的部分,往往也有糟糕的部分。鼓吹者倾向于谈论前者,并撰写有关其他编程语言中最糟糕的部分的书籍。
正如Bjarne Stroustrup(C++的发明者)正确地指出的,“只有两种语言:人们抱怨的语言和没人使用的语言”。任何自称只热爱他们所选择的编程语言的人都可能不诚实。每一种语言都是在速度、简单性、完整性、表达性、安全性和其他方面之间进行权衡。
我是Lua的超级粉丝,Lua通过严格限制自己来避免了大量的复杂性。剩下的是一种强大的编程语言,可以很容易地解释、编译和嵌入。但这是要付出代价的——你真的可以在20分钟内学会Lua,但这就是全部。
在另一端的是现代c++(2017年),它决定尝试成为所有人的所有东西:快速、强大、完整和高度表达。成本是显而易见的:如果您使用所有的特性,就会有巨大的复杂性。
在这篇和以后的文章中,我希望说服C程序员给‘2017时代的C++’(完全不像2003年的C++)一个好印象。为此,我想展示的是,在C++中隐藏了一种简单的语言,它仍然为您提供了许多好东西,而不会立即要求您处理“C++编程语言”的全部1400页。换句话说,我认为,只要明智地选择C++的精华部分,就已经有了很大的好处。
我的目标是,当你去寻找一种新的语言学习(比如go或Rust)时,希望你也能考虑使用现代C++。
在这个系列
在这一系列的文章中,我的目标是C++2014,编译器可以广泛使用。偶尔的C++2017特性可能会出现。本系列将介绍C++中立即有用的部分,这些部分C程序员可以从中受益,而无需使用“完整的”C++。目标是使开发人员能够从C++“一次一行”中获益。
具体地说,我将不包括:
- 多重继承
- 模板元编程
- C++ iostreams(除标准输出外,其余都是stdio)
- C++语言环境(使用C语言环境)
- 用户定义的文字
- “外来的”
C和C++之间的关系
C和C++实际上是非常密切的关系,以至于许多编译器都有统一的语言基础结构。换句话说,您的C代码已经通过与c++共享的代码页(可能是用C++编写的)。实际上,当普通的C程序被编译为C++和g++时,就会出现大小相同的二进制文件。我们喜爱的C编程语言中的所有示例程序都被编译为有效的C++。有趣的是,1988年K&R notes Bjarne Stroustrup的C++“translator”被广泛用于本地测试。
这种关系更进一步——整个C库都包含在C++“by reference”中,C++知道如何调用所有的C代码。相反,完全有可能从C调用C++函数。
与C相比,C++明确地设计成不存在不可避免的开销。
零开销原则是C++设计的指导原则。它指出:你不使用的东西,你不需要付费(在时间或空间上),进一步说:你使用的东西,你无法更好地编写代码。
换句话说,没有特性应该被添加到C++这将使任何现有代码(不使用新特性)较大或较慢,也不应该任何特性被添加的编译器会生成代码,不如一个程序员将创建不使用功能。
这些都是很大的要求,他们确实需要一些证据。要想在2018年实现这一目标,我们必须小心。很多代码都使用异常,而且这些异常也会带来一些开销。但是,也可以声明我们代码的所有或部分是无异常的,这将导致编译器删除该基础结构。
但这里有实际的证据。使用Cqsort()
函数对1亿个整数进行排序,使用C++中的std::sort()
和使用C+±2017并行排序,我们得到以下时间:
C qsort(): 13.4 seconds (13.4 CPU)
C++ std::sort(): 8.0 seconds (8.0 CPU)
C++ parallel sort: 1.7 seconds (11.8 seconds of CPU time)
这是什么魔法?C++版本比C快40%?这怎么可能?
这是代码:
int cmp(const void* a, const void* b)
{
if(*(int*)a < *(int*)b)
return -1;
else if(*(int*)a > *(int*)b)
return 1;
else
return 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);
else 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,但之后我们看到了第一个oddity:auto
。我们稍后将对此进行介绍,但是auto
几乎总是做您认为它做的事情:计算所需的类型并使用它。
接下来的两行定义一个包含整数的向量,并在其中保留足够的空间以满足我们需要的条目数。这是一个可选的优化。然后while循环用“随机数”填充向量。
接下来神奇的事情发生了。我们调用Cqsort()
函数,对包含数字的C++向量进行操作。这怎么可能?事实证明,std::vector
被显式地设计成可以与原始指针操作互操作。它可以被传递到C库或系统调用。它将数据存储在可随意更改的连续内存中。
接下来的4行使用C++排序函数。在g++的某些版本中,您可能需要这个(非标准)语法来获得相同的结果:__gnu_parallel: sort(vec.begin()、vec.end())。
那么为什么c++ std::排序比qsort快?
qsort()
是一个接受比较回调的库函数。因此,编译器(及其优化器)不能将qsort()
过程视为一个整体。此外,还有函数调用开销。
与此同时,sort版本实际上是一个“模板”,它可以内联比较谓词,这是为ints默认为<操作符的。
为了确保我们是公平的,因为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++最初开发的大部分时间里,它没有string类。写这样一门课多少有点像一种仪式,每个人都有自己的方式。这背后的部分原因是,人们长期试图建立一个对每个人都至关重要的班级。
字符串c++在1998年与C代码很好地交互:
std::string dir("/etc/"), fname;
fname = dir + "hosts";
FILE* fp = fopen(fname.c_str(), "r");
std::string提供了您所期望的大多数功能,比如连接(如上所示)。一些进一步的代码:
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());
string通过[]操作符提供对其字符的不安全且不检查的访问,所以newname[0] = ‘/’,但聪明人使用newname.at(0)来执行边界检查。
2011年后std::string的设计很有趣。基本字符串实现的存储如下:
struct mystring
{
char* data;
size_t len;
size_t capacity; // how much we've allocated already
};
在现代系统中,这是24字节的数据。容量字段用于存储分配了多少内存,以便mystring知道何时需要重新分配内存。不是每次在字符串中添加字符时都重新分配是一个很大的胜利。
然而,我们在字符串中存储的东西通常比24字节要短得多。由于这个原因,现代c++ std::string实现实现实现了小字符串优化,允许它们在自己的存储中存储16字节甚至21字节的字符,而不使用malloc(),这是一种加速。
防止不必要地调用malloc()的另一个好处是,字符串数组现在存储在连续的内存中,这对于内存缓存命中率来说是很好的,它通常会传递所有的加速因子。
经过多年的设计,std::string对每个人来说可能不是万能的,但与“零开销”原则相一致,它打败了你可以快速手写的东西。
总结
在本系列的第一部分中,我希望向您展示了一些有趣的C++代码,您可以立即开始使用它们——在不立即将复杂的代码填满的情况下获得大量新功能。
第2部分可以在这里找到。
如果你有任何你喜欢的东西,你希望看到讨论或问题,请联系@PowerDNS_Bert或bert.hubert@powerdns.com