Amethyst Studio
2413 words
12 minutes
C++ ranges - Quick Start
2024-03-01

A Problematic Code#

看下面的代码:

#include <vector>
#include <iostream>
#include <iterator>

std::vector<int> getVector() {
  std::vector<int> v {1,2,3};
  return v;
}

int main() {
  std::copy(getVector().begin(), getVector().end(), std::ostream_iterator<int>(std::cout, " "));
  return 0;
}

上面的代码,很显然是有问题的,因为getVector这个函数会创造一个临时的vector,这里调用了两次getVector,也就意味着出现两个临时的vector对象,那么前后两次使用迭代器begin()end(),就并不是同一个对象的起点和终点。于是,这个代码就很容易出现未定义的行为。

本质上来说,这里的copy,跟下面的代码差不多:

std::copy(a.begin(), b.end(), /*...*/)

但是麻烦的是,这样的代码却是可以编译通过的,无论是g++编译器还是clang++编译器,都没有提出错误或者警告信息。我们只有在运行的时候,才知道这里出现了问题。

而STL大量地使用这种范式,无论是这里copy或者transform,都需要接收两个迭代器参数,这样的情况下,编译器有时很难检测这两个迭代器是否来自于同一个对象,这样为整个程序实际上都带来了隐患。

因此从C++20开始,C++引入ranges,来避免使用迭代器模式,而是使用类似于函数式编程的范式来处理日常的工作。

overview#

日常我们可能会需要这样的任务,给定一个vector,我们需要每一个元素筛选出符合条件的元素,接着应用某一个函数再返回出一个新的vector。古老的做法可能是使用for循环,C++11之后使用STL可能会是使用接口filtertransform,对于C++20引入的ranges,我们可以这么做:

std::vector<int> v;
auto view = std::views::all(v) 
		| std::views::filter([](int n) { /*...*/ })
		| std::views::transform([](int n){/*...*/});
		| std::ranges::to<std::vector<int>>();
		;

就像这样,ranges允许你使用这种管道和运算的组合来完成各种操作。这种函数式的写法比先前的for循环或者STL的写法要更加便捷和优雅,也因为使用这种函数式编程,它也更容易在底层进行优化。

注意:上面的代码中,可以去掉std::views::all(v)而直接使用v,这是C++标准库为STL数据结构提供的一个便利。

下面介绍一些常用的ranges API。

1. 通用ranges API#

1.1 std::views::iota#

std::views::iota生成一个从指定值开始的连续整数序列。例如,生成一个从0到9的整数序列:

#include <iostream>
#include <ranges>
#include <iterator>

int main() {
    auto vec = std::views::iota(0, 10);
    std::copy(vec.begin(), vec.end(), std::ostream_iterator<float>(std::cout, " "));
    std::cout << '\n';
    // 0 1 2 3 4 5 6 7 8 9
    return 0;
}

1.2 std::views::take#

std::views::take用于从序列的开始处获取指定数量的元素。例如,从上面的整数序列中取前5个数字:

#include <iostream>
#include <ranges>
#include <iterator>

int main() {
    auto vec = std::views::iota(0, 10) | std::views::take(5);
    std::copy(vec.begin(), vec.end(), std::ostream_iterator<float>(std::cout, " "));
    std::cout << '\n';
    // 0 1 2 3 4
    return 0;
}

1.3 std::views::drop#

std::views::drop用于从序列的开始处跳过指定数量的元素。例如,跳过前3个元素:

#include <iostream>
#include <ranges>
#include <iterator>

int main() {
    auto vec = std::views::iota(0, 10) | std::views::drop(3);
    std::copy(vec.begin(), vec.end(), std::ostream_iterator<float>(std::cout, " "));
    std::cout << '\n';
    // 3 4 5 6 7 8 9
    return 0;
}

1.4 std::views::transform#

std::views::transform应用一个函数到序列中的每个元素上。例如,将每个元素乘以2:

#include <iostream>
#include <ranges>
#include <iterator>

int main() {
    auto vec = std::views::iota(0, 10)
             | std::views::transform([](int n) { return n * 2; });
    std::copy(vec.begin(), vec.end(), std::ostream_iterator<float>(std::cout, " "));
    std::cout << '\n';
    // 0 2 4 6 8 10 12 14 16 18
    return 0;
}

1.5 std::views::filter#

std::views::filter根据给定的谓词函数过滤序列。例如,筛选出偶数:

#include <iostream>
#include <ranges>
#include <iterator>

int main() {
    auto vec = std::views::iota(0, 10)
             | std::views::filter([](int n) { return n % 2 == 0; });
    std::copy(vec.begin(), vec.end(), std::ostream_iterator<float>(std::cout, " "));
    std::cout << '\n';
    // 0 2 4 6 8
    return 0;
}

1.6 std::views::reverse#

std::views::reverse反转序列。例如,反转整数序列:

#include <iostream>
#include <ranges>
#include <iterator>

int main() {
    auto vec = std::views::iota(0, 10)
             | std::views::reverse;
    std::copy(vec.begin(), vec.end(), std::ostream_iterator<float>(std::cout, " "));
    std::cout << '\n';
    // 9 8 7 6 5 4 3 2 1 0
    return 0;
}

1.7 std::views::repeat (C++23)#

std::views::repeat用于创造重复的序列。

#include <iostream>
#include <ranges>
#include <iterator>

int main() {
    auto vec = std::views::repeat(1, 10);
    std::copy(vec.begin(), vec.end(), std::ostream_iterator<float>(std::cout, " "));
    std::cout << '\n';
    // 1 1 1 1 1 1 1 1 1 1
    return 0;
}

1.8 std::views::all#

std::views::all用于创建一个包含整个序列的视图。例如,简化对整个向量的访问,让向量里面的元素参与ranges运算。

#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    auto all = std::views::all(vec);
	std::copy(all.begin(), all.end(), std::ostream_iterator<float>(std::cout, " "));
    std::cout << '\n';
	// 1 2 3 4 5
    return 0;
}

1.9 std::views::empty#

std::views::empty用于生成一个空的视图。在一些特殊情况下可能会需要(想一想使用nullptr的场景)。

#include <iostream>
#include <ranges>

int main() {
    auto empty_view = std::views::empty<int>;
    return 0;
}

1.10 std::views::single#

std::views::single用于创建一个只包含单个元素的视图。例如:

#include <iostream>
#include <ranges>

int main() {
    auto single_view = std::views::single(42);
    return 0;
}

2. 字符串ranges API#

split操作和join操作虽然也可以用在别的容器上,但是最一般的情况,应该还是用在字符串操作上。

注意:使用string_view而不是string。

2.1 std::views::split#

std::views::split用于根据指定的分隔符将字符串拆分成多个部分。例如,拆分一个字符串:

#include <iostream>
#include <ranges>
#include <string_view>

using namespace std::literals;

int main() {
    std::string_view s = "this is a temp file"sv;
    auto words = std::views::split(s, " "sv);
    for (const auto & w: words) {
        std::cout << std::string_view(w) << '\n';
    }
	return 0;
}

2.2 std::views::join#

std::views::join用于将多个序列连接成一个序列。例如,连接多个字符串:

#include <iostream>
#include <vector>
#include <ranges>
#include <string_view>

using namespace std::literals;

int main() {
    std::vector<std::string_view> words {"Hello"sv, "this"sv, "is"sv, "a"sv, "simple"sv, "exmaple"sv};
    for(auto &w: words|std::views::join) {
        std::cout << w;
    }
    return 0;
}

2.3 std::views::join_with(C++23)#

std::views::join用于将多个序列连接成一个序列,并且使用连接符号:

#include <iostream>
#include <string>
#include <vector>
#include <ranges>

int main() {
    std::vector<std::string> words = {"Hello", "World"};
    auto joined = words | std::views::join_with(", ");
    for (char c : joined) {
        std::cout << c;
    }
    return 0;
}

3. 映射ranges API#

3.1 views::keys_view#

views::keys_view用于从映射中提取键的视图。例如,从一个std::map中提

取键:

#include <iostream>
#include <map>
#include <ranges>
#include <iterator>

int main() {
    std::map<std::string, int> m = {{"one", 1}, {"two", 2}, {"three", 3}};
    auto keys = m | std::views::keys;
	std::ranges::copy(keys, std::ostream_iterator<std::string>(std::cout, " "));
    std::cout << '\n';
    // one two three 
    return 0;
}

3.2 views::values_view#

views::values_view用于从映射中提取值的视图。例如,从一个std::map中提取值:

#include <iostream>
#include <map>
#include <ranges>
#include <iterator>

int main() {
    std::map<std::string, int> m = {{"one", 1}, {"two", 2}, {"three", 3}};
    auto values = m | std::views::values;
	std::ranges::copy(value, std::ostream_iterator<int>(std::cout, " "));
    std::cout << '\n';
    // 1 3 2
    return 0;
}

4. C++23 C++26 ranges API#

以下介绍一些在C++23和C++26中才会出现的ranges操作,截止本博客写成之时,仍然有部分API没有实现,它真正的实现和用法可能会与以下的内容不同。未来博客会再次更新。

4.1 views::enumerate#

views::enumerate将序列的元素和其对应的索引配对,生成一个包含索引和元素的视图。例如,枚举一个序列的元素:

#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<int> vec = {10, 20, 30};
    auto enumerated = vec | std::views::enumerate;
    for (auto &[index, value] : enumerated) {
        std::cout << "Index: " << index << ", Value: " << value << "\n";
    }
    // Index: 0, Value: 10
    // Index: 1, Value: 20
    // Index: 2, Value: 30
    return 0;
}

4.2 views::zip#

views::zip用于将多个序列的对应元素组合成一个新的序列。例如,将两个序列的元素组合:

#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<int> vec1 = {1, 2, 3};
    std::vector<std::string> vec2 = {"one", "two", "three"};
    auto zipped = std::views::zip(vec1, vec2);
    for (auto &[num, str] : zipped) {
        std::cout << num << ": " << str << "\n";
    }
	// 1: one
	// 2: two
	// 3: three
    return 0;
}

4.3 views::adjacent#

views::adjacent用于将序列中相邻的元素组合成一个新的序列。例如,将相邻元素配对:

#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    auto adjacent_pairs = vec | std::views::adjacent(2);
    for (auto [a, b] : adjacent_pairs) {
        std::cout << a << ", " << b << "\n";
    }
	// 1, 2
	// 2, 3
	// 3, 4
	// 4, 5
    return 0;
}

4.4 views::slide#

views::slide创建一个滑动窗口视图,其中每个窗口包含序列中的连续元素。例如,创建三个元素的滑动窗口:

#include <vector>
#include <ranges>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5, 6};
    auto sliding_windows = vec | std::views::slide(3);
    for (auto window : sliding_windows) {
        for (int x : window) {
            std::cout << x << " ";
        }
        std::cout << "\n";
    }
    return 0;
}

这里,每个输出的窗口包含序列中的三个连续元素,展示了如何通过滑动窗口捕获和处理数据的局部特征。

4.5 views::chunk#

views::chunk将序列分割成指定大小的块。例如,将一个序列分割成每块包含两个元素的块:

#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5, 6};
    auto chunks = vec | std::views::chunk(2);
    for (auto chunk : chunks) {
        for (int x : chunk) {
            std::cout << x << " ";
        }
        std::cout << "\n";
    }
    return 0;
}

这个例子显示了如何将一个更大的序列分割成更小的、更易于管理的单元,便于进行并行处理或者更细粒度的数据分析。

4.6 views::stride#

views::stride从序列中以指定的步长取元素,生成新的视图。例如,从一个整数序列中每隔一个元素取一个:

#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5, 6};
    auto strided = vec | std::views::stride(2);
    for (int x : strided) {
        std::cout << x << " ";
    }
    return 0;
}

在这个例子中,输出为“1 3 5”,展示了如何跳过一定数量的元素来访问序列,这可以用于降低数据密度或者处理数据时跳过不必要的部分。

Summary#

这些例子展示了如何利用C++20和预期中的C++23中的ranges API来简化和增强数据处理的能力,通过提供更声明式、更易读且安全的方法来操作序列。ranges不仅提高了代码的可维护性,还降低了错误发生的概率,是现代C++中处理序列数据的强大工具。

C++ ranges - Quick Start
https://ziyue.cafe/posts/cpp20-ranges-quickguide/
Author
Kaida Amethyst
Published at
2024-03-01