面向C程序员的现代C++(一)

面向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或[email protected]

:原文MODERN C++ FOR C PROGRAMMERS: PART 1.

猜你喜欢

转载自blog.csdn.net/xzwspy/article/details/80752572