2

C++ 变化太大!该重新学习这门语言了

 1 year ago
source link: https://www.techug.com/post/the-addition-of-c-has-changed-too-much-it-s-time-to-relearn-this-languaged4f57fb68153c0140bd0/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client



the-addition-of-c-has-changed-too-much-it-s-time-to-relearn-this-languaged4f57fb68153c0140bd0.01.jpeg

C++是一门古老但不断演进的语言。你几乎可以使用它来做任何事情,而且可以在很多地方找到它的身影。实际上,C++的发明者 Bjarne Stroustrup 将其描述为一切事物的隐形基础。有时,它可以深入到另外一门语言的库中,因为 C++可以用于性能关键的路径中。它可以在小型的嵌入式系统中运行,也可以为视频游戏提供动力。你的浏览器可能正在使用它。C++几乎无处不在!

C++为何如此重要

迄今为止,C++已经存在了很长的时间,但是其变化也是非常大的,尤其是 2011 年之后。当时,推出了一个名为 C++11 的新标准,标志着一个频繁更新的时代正式开启。如果你从 C++11 就没有使用过 C++,那么你有很多东西需要补习,这要从哪里开始呢?

该语言是需要编译的,面向特定的架构,如 PC、大型机、嵌入式设备、定制硬件,或者你想到的其他东西。如果你需要代码在不同类型的机器上运行,那需要重新编译它。这有缺点也有优点。不同的配置会带来更多的维护工作,但编译到特定架构能够让你“因地制宜(down to the metal)”,从而获得速度方面的优势。

不管你的目标是哪种平台,均需要一个编译器。你还需要一个编辑器或集成开发环境(IDE)来编写 C++代码。ISOCpp给出了一个资源清单,包括 C++编译器。Gnu 编译器集(Gnu compiler collection,gcc)、Clang 和 Visual Studio 均有免费版本。你甚至可以使用Matt Godbolt的编译器探索器,在浏览器上尝试基于各种编译器的代码。编译器可能支持不同版本的 C++,所以必须在编译器标记中说明你所需要的版本,例如 g++的-std=c++23或 Visual Studio 的/std:c++latest。ISOCpp 网站上有一个FAQ区域,概述了最近的一些变化,包括 C++11 和 C++14,以及整体的概览。另外,还有多本关于 C++最近版本的图书。

使用 Vector 快速了解 C++11

如果你已经被落下了,那么大量的资源可能会让你不知所措。但是,我们可以通过一个小例子来理解一些基础知识。停下来,亲自动手试一试往往是最好的学习方法。因此,我们从简单基础的东西开始吧!

一个很有用(且简单)的起点是不太起眼的vector,它位于std命名空间的vector头文件中。CppReference 提供了一个概述,告诉我们vector是一个序列容器,封装了动态大小的数组。因此,vector包含了一个连续的元素序列,我们可以根据需要调整 vector 的大小。vector本身是一个类模板,因此它需要一个类型,例如std::vector<int>。我们可以使用push_back将一个条目添加到 vector 的尾部。C++11 引入了一个名为emplace_back的新方法,该方法取值来构造一个新的条目。对于int,代码看上去是一样的:

std::vector<int> numbers;
numbers.push_back(1);
numbers.emplace_back(1);

如果我们有比int更复杂的东西,那么就可能在 emplace 版本中获得性能方面的收益,因为 emplace 版本可以就地构造条目,从而避免对其进行复制。

C++11 引入了_r-value 引用_和_移动语义(move semantics)_来避免不必要的复制。潜在的性能改善是 C++11 的驱动力之一,后续的版本都是在此基础上进行的。为了解释什么是 r-value 引用,我们可以考虑前面样例中的push_back方法。它有两个重载形式,其中一个会接受一个常量引用,即const T&值,另外一个接受一个 r-value 引用,即T&&值。第二个版本会将元素移动到 vector 中,这可以避免复制临时对象。与之类似,emplace_back的签名通过 r-value 引用来获取参数,Args&&…,同样允许移动参数而无需复制。移动语义是一个很大的话题,我们只是接触到了它的皮毛。如果你想了解更多详情的话,Thomas Becker 在 2013 年撰写了一篇很好的文章,介绍了它的细节。

我们创建一个vector并在其中放置几个条目,然后使用来自iostream头文件的std::cout展示其内容。我们使用流插入操作符<<来显示这些元素。我们基于vectorsize编写一个for循环,并使用操作符[]来访问每个元素:

#include <iostream>
#include <vector>


void warm_up()
{
    std::vector<int> numbers;
    numbers.push_back(1);
    numbers.emplace_back(1);
    for(int i=0; i<numbers.size(); ++i)
    {
        std::cout << numbers[i] << ' ';
    }
    std::cout << '\n';
}


int main()
{
    warm_up();
}

该代码会显示两个 1。这段代码可以在编译器探索器上找到。

类模板参数推断

让我们做一些更有意思的事情,并学习一下现代的 C++。我们构建几个数字三角,会发现它们之间存在一个模式。数字三角的值是 1,3,6,10……它们分别由 1,1+2,1+2+3,1+2+3+4,……相加而成。如果我们这些斯诺克球架起来,就可以组成一个三角形,它也因此得名:

如果再增加一排,我们就会再增加六个斯诺克球。再加一排就会增加七个,以此类推。

为了得到数字 1,2,3 等,我们可以构建一个充满 1 的 vector,然后将这些数字相加。我们可以直接创建一个 vector,比如 18 个 1,而不必再增加另一个循环。我们说明想要多少个元素,然后再指明它的值:

   std::vector numbers(18, 1);

注意我们不需要再声明<int>了。因为从 C++17 开始,_类模板参数推断(CTAD)就已经实现了。编译器可以推断出我们指的是int,因为我们要求的值是 1,这是一个int。如果我们需要显示 vector,那么可以使用_基于 range 的 for 循环。此时,我们不必使用基于 vector 索引的传统for循环,而是声明一个类型,甚至可以使用新的关键字auto,告诉编译器判断类型,然后是冒号和容器:

   for (auto i : numbers)
    {
        std::cout << i << ' ';
    }
    std::cout << '\n';   

CTAD 和基于 range 的for循环是 C++11 以来引入的一些便利特性。

Range

有了由“1”组成的 vector,我们就可以包含numeric头文件,并使用部分的和来填充一个新的vector,如 1,1+1,1+1+1……,这样就有了 1,2,3……我们需要声明新vector的类型,因为这里要从一个空的vector开始,如果没有任何值可供使用,那么编译器将无法推断其类型。partial_sum需要开头和结尾的数字,最后我们需要使用back_inserter,这样目标 vector 会根据需要增长:

    #include <algorithm>
…
    std::vector numbers(18, 1);
    std::vector<int> sums;
    std::partial_sum(numbers.begin(), numbers.end(),
        std::back_inserter(sums));

这样我们就得到了 1 到 18 的数字,均包含边界值。我们已经完成了数字三角的部分工作,但是 C++现在可以让我们的代码更加简洁。C++11 引入了iota函数,也位于numeric头文件中,它能够用不断增加的值填充一个容器:

std::vector<int> sums(18);
std::iota(sums.begin(), sums.end(), 1);

实际上,C++23 引入了一个 range 版本,它会为我们找到对应的beginend

  std::ranges::iota(sums, 1);

C++23 还没有得到广泛的支持,所以可能需要等到你的编译器提供 range 版本。numericalgorithm头文件中的很多算法都有两个版本,其中一个需要一对输入迭代器(即first and last),另一个则是 range 版本,只需要接受容器即可。ranges 重载正在逐渐添加到标准 C++中。ranges 提供的功能远远超过我们这里避免声明两个迭代器的场景。我们可以过滤和转换输出,将这些东西连接在一起,并使用视图来避免复制数据。ranges 支持惰性计算,所以视图的内容会在需要的时候才评估计算出来。Ivan Čukić的Functional Programming in C++一书在这方面提供了更多的细节(书中还包含更多的内容)。

我们需要做的最后一件事就是形成数字三角。查看 vector 的部分和:

   std::partial_sum(sums.begin(), sums.end(), sums.begin());

我们已经得到了想要的数字三角,即 1,3,6,10,15……171。

我们注意到,有些算法有 ranges 版本,那我们可以尝试一个。前两个三角数字是 1 和 3 是奇数,然后是两个偶数 6 和 10。这个模式是不是可持续的呢?如果我们对 vector 进行转换,用点号“.”来标记奇数,用星号“*”来标记偶数,就能看出最终结果。我们可以声明一个新的 vector 来存放转换结果。对于每个数字,仅需要一个字符,所以我们需要一个char类型的vector

std::vector<char> odd_or_even.

我们可以编写一个简短的函数,它会获取一个 int 并返回对应的字符:

char flag_odd_or_even(int i)
{
    return i % 2 ? '.' : '*';
}

如果i % 2的值不为零,这就是一个奇数,所以我们返回.,否则,返回*。我们可以在来自algorithm头文件的transform函数中使用这个自己的函数。最初的版本需要一对输入迭代器(first 和 last)、一个输出迭代器和一个_一元函数(unary function)_,该函数会接受一个输入,就像我们的flag_odd_or_even函数这样。C++20 引入了一个 ranges 版本,它能够接受一个输入源,而不是一对迭代器,另外还需要一个输出迭代器和一元函数。这意味着我们可以通过如下方式来转换先前生成的和:

   std::vector<char> odd_or_even;
    std::ranges::transform(sums,
        std::back_inserter(odd_or_even),
        flag_odd_or_even);

输出将会如下所示:

. . * * . . * * . . * * . . * * . .

看上去,我们确实是不断地得到两个奇数,然后是两个偶数。Stack Exchange 的数学网站阐述了出现这种现象的原因

Lambdas

我们使用另一个新的 C++特性对我们的代码做最后的改进。如果我们想要看一下实际的转换代码的话,那需要要转移到另外一个地方才能看到这个一元函数都做了些什么。

C++11 引入了匿名函数或lambda表达式的特性。它们看起来与有名称的函数类似,将参数放在括号中,将函数主体放到花括号中,但是它们没有名字,不需要返回类型,并且有一个用[]表示的捕获组:

[](int i) { return i%2? '.':'*'; }

如果与有名称的函数进行对比,会看到两者的相似性:

char flag_odd_or_even(int i){ return i % 2 ? '.' : '*'; }

我们可以在捕获组中声明变量,这会给我们一个_闭包_。这些内容超出了本文的范围,但是在函数式编程中它们是非常强大和常见的。

如果我们将一个 lambda 分配给一个变量,

auto lambda = [](int i) { return i % 2 ? '.' : '*'; };

那么,我们就可以像调用有名称的函数那样调用它:

lambda(7);

这个特性允许我们使用 lambda 重写转换调用:

    std::ranges::transform(sums,
        std::back_inserter(odd_or_even),
        [](int i) { return i%2? '.':'*'; });

这样的话,我们就可以在一个地方看到转换函数,而不必再去查看其他的地方了。

将所有的内容组合在一起,就形成了如下的代码:

#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>


int main()
{
    std::vector<int> sums(18);
    std::iota(sums.begin(), sums.end(), 1);
    std::partial_sum(sums.begin(), sums.end(), sums.begin());


    std::vector<char> odd_or_even;
    std::ranges::transform(sums,
        std::back_inserter(odd_or_even),
        [](int i) { return i%2? '.':'*'; });


    for (auto c : odd_or_even)
    {
        std::cout << c << ' ';
    }
    std::cout << '\n';
}

我们使用了 ranges、lambda 和基于 range 的for循环,浏览了移动语义,并练习了对 vector 的使用。对于首次重回 C++的人来说,这是一个不错的起点!

你可以在编译器探索器中尝试上述的代码

作者简介:

Frances Buontempo 有多年的 C++经验,还有过使用 Python 和其他各种语言的经验。她曾发表过关于 C++的演讲,并且是 ACCU 的 Overload 杂志的编辑。她有数学背景,为 PragProg 写了一本关于遗传算法和机器学习的书,并且正在为 Manning 写一本名为 C++ Bookcamp 的 C++书,以帮助那些被现代 C++落下的人迎头赶上。

原文链接:

Relearning C++ After C++11

本文文字及图片出自 InfoQ


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK