for循环的演变:从C++03到C++20

文摘   2024-09-11 12:06   北京  

你好,我是雨乐~

C++近些年发展迅速,相较于C++11之前的版本,C++11标志着Modern C++新时代的到来。在这个新时代中,C++语言引入了大量的创新特性,如智能指针、范围for循环、自动类型推导、并发支持以及更强大的模板功能等,各种新特性如雨后春笋般涌现。这些新特性大大提高了程序的安全性和效率,同时也使得代码的编写和维护变得更加简洁和高效。紧接着,C++14、C++17、C++20和即将到来的C++23等版本继续推动语言的演进,不断引入新的功能和改进,使得C++在现代编程领域中保持了强劲的生命力和竞争力。

在这个演变过程中,某些之前已有的功能,也因为需求的变化而不断在变化,今天,就以我们常用的一个功能for循环,来聊聊其在C++发展中的演变过程~

Base

我们先回顾下最最基础的循环。

相信很多人学C或者C++,都是从下面这种方式开始的:

for (int i = 0; i < 10; ++i) {
// do sth
}

对于容器的访问,可以像如下这样:

std::vector<int> v = {//xxx};

for (int i = 0; i < v.size(); ++i) {
  std::cout << "v[" << i << "] is " << v[i] << std::endl;
}

当然了,对于那些不支持下标访问的容器,例如std::map,可以使用如下这种方式进行遍历:

std::map<int,int> m ={{1,2},{2,3},{3,4}};

std::map<int,int>::iterator it = m.begin();
for(; it != m.end();++i){
  std::cout <<"key is "<< it.first <<" value is: "<< it.second << std::endl;
}

好了,截止目前为止,我们已经了解了基本的for循环,这些for循环方式是C++11之前常用的,可以看出其写法得多冗余,正式基于这个原因,随着标准的不断迭代升级,for循环也相应的向易用性靠拢,下面我们聊聊for循环在各个标准中的演变过程~

C++11

C++11中,主要使用了autorange方式提升其使用方式。

auto

C++11中,引入了auto关键字进行类型推断。auto 关键字可用于各种上下文,包括使for循环更简洁。

仍然以我们前面的map遍历为例:

// before
std::map<int,int>::iterator it = m.begin();
for(; it != m.end();++i){
  std::cout <<"key is "<< it.first <<" value is: "<< it.second << std::endl;
}

// C++11
for(auto it = m.begin(); it != m.end();++i){
  std::cout <<"key is "<< it.first <<" value is: "<< it.second << std::endl;
}

从上面的代码来看,复杂的变量声明被一个auto关键字所替代,即(std::map<int, int>::iterator it = m.begin()不复存在)。

range-based

除了使用auto关键字来简化复杂变量声明之外,基于范围的for循环提供了另一种简便的方式来遍历容器中的元素。这种语法大大提高了代码的可读性和简洁性,减少了错误的可能性。

具体来说,基于范围的for循环的语法如下:

for (auto element : container) {
  //do sth
}

同样,对于前面map的遍历,用range可以像下面这样写:

for (auto &&it : m) {
  std::cout << "key is " << it.first << " value is: " << it.second << std::endl;
}

C++17

emm,你或许会问,C++14呢?

且不说for循环使用方式在C++14中与C++11使用上无异,因其低微的存在感(较C++11变化不是很大),以至于各种教材没有对其进行单独的介绍,所以在升级编译器的时候,都建议忽略14直接到17乃至20~

好了,废话不多说,开始正文吧

结构化绑定(Structured Binding),算是C++17的一个比较显眼的特性,其使用方式使我们摒弃了之前各种复杂冗余的操作。以std::pairstd::tuple为例,为了获取其内容,在之前的版本中,往往需要使用first、second或者std::get<n>获取其元素,C++17引入的结构化绑定则简化了上述方式。

auto p = std::make_pair(12);

auto [x, y] = p; // x = 1, y = 2
auto& [r_x, r_y] = p; // r_x 为p.first的引用,r_y为p.second的引用

相应的,我们也可以使用结构化绑定和基于范围的for循环 方式简化遍历:

for (auto &&[x, y] : m) {
  std::cout << "key is " << x << " value is: " << y << std::endl;
}

当然了,也可以使用-来忽略某个值,比如:

for (auto &&[-, y] : m) {
  std::cout << " value is: " << y << std::endl;
}

Ranges & Views

C++20引入了范围库(Ranges Library),这是对 C++ 标准库的重要扩展。范围库使得处理序列(如容器、数组、流等)的代码更加简洁和易读。范围库提供了一种新的方式来处理数据序列,比传统的算法和迭代器方法更为直观和强大。

先从一个简单的例子入手,假如有一个类型为int的vector,请输出其元素*2之后的结果,大概率我们会这么做:

std::vector<int> numbers = {12345};
for (auto i : numbers) {
  std::cout << i * 2 << std::endl;
}

或者结合lambda表达式:

std::vector<int> numbers = {12345};
std::for_each(numbers.begin(), numbers.end(), [](auto item) {std::cout << 2 * item << std::endl;});

如果使用C++20的Ranges,则可以像如下这么写:

    std::vector<int> numbers ={1,2,3,4,5};

// 使用范围库的视图
auto doubled = numbers | std::views::transform([](int n){return n *2;});

for(int num : doubled){
        std::cout << num <<" ";
}

如果要获取numbers的最后三个元素,则可以:

for (int v : std::ranges::views::reverse(std::ranges::views::take(std::ranges::views::reverse(numbers),3))) { 
    std::cout << v << ", "

也可以像如下这么写:

for (int v : numbers | std::ranges::views::reverse | std::ranges::views::take(3) | std::ranges::views::reverse) { 
    std::cout << v << ", "

结语

从C++11到C++20,for循环经历了显著的演化和改进。C++11引入的基于范围的 for 循环极大地简化了代码的编写,C++14继续支持这一特性,C++17通过结构化绑定使得编码更加便捷,C++20则通过范围库和其他特性进一步提升了代码的简洁性和功能性。这些改进使得C++语言在现代编程实践中保持了强劲的竞争力和高效的开发体验。

以上~~

如果对本文有异议或者有其他技术问题,可以加微信交流:

推荐阅读  点击标题可跳转

1、性能大杀器:与std::endl的较量

2、性能大杀器:智能指针的资源管理

3、性能大杀器:lambda的前世今生

雨乐聊编程
毕业于中国科学技术大学,现任某互联网公司高级技术专家一职。本公众号专注于技术交流,分享日常有意思的技术点、线上问题等,欢迎关注