为什么需要 Ranges?

在 C++20 之前,处理数据序列是这样的:

// 传统 STL:冗长、易错
std::vector<int> nums = {1, 2, 3, 4, 5, 6};
std::vector<int> result;
for (int n : nums) {
    if (n % 2 == 0) {
        result.push_back(n * n);
    }
}
// 或者用算法(更复杂)
std::vector<int> result;
std::copy_if(nums.begin(), nums.end(), std::back_inserter(result),
    [](int n) { return n % 2 == 0; });
std::transform(result.begin(), result.end(), result.begin(),
    [](int n) { return n * n; });

有了 Ranges,代码变得如此简洁:

// Ranges:声明式、可读性强
auto result = nums
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::transform([](int n) { return n * n; })
    | std::ranges::to<std::vector>();  // C++23

核心优势

  • 管道式操作:用 | 链式组合,像 Unix 管道一样直观
  • 懒惰求值:只在需要时计算,避免不必要的中间容器
  • 直接操作容器:无需 begin()/end() 迭代器对
  • 投影支持:轻松操作结构体成员,无需复杂 lambda
  • 类型安全:编译期检查,防止悬空迭代器

核心概念速览

Range(范围)

任何可迭代的序列,只要提供 begin()end()

  • 容器:std::vectorstd::arraystd::string
  • 视图:std::string_viewstd::span
  • C 风格数组

View(视图)

轻量级、懒惰的范围表示:

  • 不复制数据,只保存引用
  • 按需计算,直到迭代时才执行
  • 可组合,用 | 管道连接

Adaptor(适配器)

修改范围而不改变底层数据:

  • views::filter - 过滤
  • views::transform - 转换
  • views::take - 取前 N 个
  • views::reverse - 反转

Algorithm(算法)

Ranges 版本的算法,直接接受范围:

  • std::ranges::sort(vec) 而不是 std::sort(vec.begin(), vec.end())
  • 支持投影:std::ranges::sort(people, {}, &Person::age)

快速上手:5 分钟掌握 Ranges

基本过滤和转换

#include <print>
#include <vector>
#include <ranges>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6};
    
    // 过滤偶数并平方
    auto even_squares = nums
        | std::views::filter([](int n) { return n % 2 == 0; })
        | std::views::transform([](int n) { return n * n; });
    
    // for (int sq : even_squares) {
    //     std::cout << sq << " ";  // 输出: 4 16 36
    // }
    std::println("{}", even_squares);
}

关键点

  • | 操作符从左到右组合视图。
  • 计算是懒惰的,只在循环时执行。
  • even_squares 是视图,不拥有数据。
  • 参数传递:在 filtertransform 的 lambda 中,推荐使用 auto&&

为什么在 Adaptor 中使用 auto&&

在 Ranges 的管道操作中,元素在不同视图间传递。使用 auto&&(转发引用)是处理 Ranges 的“黄金法则”。

参数传递方式对比:

传递方式优点缺点适用场景
auto简单,安全(值拷贝)产生拷贝开销;无法处理不可拷贝类型小型内置类型(int, double
const auto&避免拷贝;只读安全无法绑定到某些代理对象(如 vector<bool> 的引用);无法修改元素传统的只读大型对象
auto&&万能绑定;零拷贝;支持代理对象语法稍显复杂Ranges Adaptor 的默认首选

核心优势:

  1. 避免拷贝:对于大型对象,直接引用比拷贝更高效。
  2. 支持修改:如果你需要通过 transform 修改元素(虽然通常建议保持纯函数风格),auto&& 可以绑定到左值。
  3. 处理代理类型:这是最关键的一点。某些视图(如 views::zipvector<bool>)返回的是代理对象(proxy objects),这些对象通常是临时值但表现得像引用。const auto& 可能会延长生命周期但无法调用非 const 成员,而 auto 会触发不必要的拷贝或编译错误。auto&& 是捕获它们的唯一正确且通用的方式。
// 推荐写法:使用 auto&& 保持通用性和性能
auto result = nums
    | std::views::filter([](auto&& n) { return n % 2 == 0; })
    | std::views::transform([](auto&& n) { return n * n; });

深入理解 auto&&:转发引用的机制

auto&& 是 C++11 引入的转发引用(Forwarding Reference),也称为万能引用。它能够根据初始化表达式的值类别自动推导为左值引用或右值引用。

推导规则:

template<typename T>
void func(T&& arg);  // T&& 是转发引用

// 当传入左值时,T 推导为左值引用类型
int x = 42;
func(x);  // T 推导为 int&,arg 类型为 int& && → int&(引用折叠)

// 当传入右值时,T 推导为非引用类型
func(42);  // T 推导为 int,arg 类型为 int&&

引用折叠规则:

T 类型T&& 实际类型
intint&&(右值引用)
int&int&(左值引用)
int&&int&&(右值引用)

在 Ranges 中的应用:

std::vector<int> nums = {1, 2, 3};

// 元素通过迭代器传递给 lambda
auto filtered = nums | std::views::filter([](auto&& n) {
    // n 的类型取决于迭代器的解引用结果
    // 对于 vector<int>,*it 返回 int&,所以 n 是 int&
    return n % 2 == 0;
});

auto&& 的陷阱与注意事项

陷阱 1:意外修改原始数据

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

// ❌ 危险:auto&& 允许修改,但 filter 应该是纯函数
auto filtered = nums | std::views::filter([](auto&& n) {
    n *= 2;  // 修改了原始 vector!
    return n % 2 == 0;
});

// nums 现在是 {2, 4, 6, 8, 10}

解决方案:

// ✅ 使用 const auto& 确保只读(如果不需要处理代理对象)
auto filtered = nums | std::views::filter([](const auto& n) {
    return n % 2 == 0;
});

// ✅ 或者使用 std::forward 保持值类别但明确意图
auto filtered = nums | std::views::filter([](auto&& n) {
    auto val = std::forward<decltype(n)>(n);  // 转发但不修改
    return val % 2 == 0;
});

陷阱 2:生命周期问题

// ❌ 危险:返回对临时对象的引用
auto getFiltered() {
    std::vector<int> temp = {1, 2, 3};
    return temp | std::views::filter([](auto&& n) {
        return n % 2 == 0;
    });  // temp 被销毁,视图悬空!
}

// ✅ 解决:立即收集结果
auto getFiltered() {
    std::vector<int> temp = {1, 2, 3};
    return temp
        | std::views::filter([](auto&& n) { return n % 2 == 0; })
        | std::ranges::to<std::vector>();  // 返回新 vector
}

陷阱 3:过度使用 std::forward

// ❌ 不必要:在 filter 中使用 std::forward
auto filtered = nums | std::views::filter([](auto&& n) {
    return std::forward<decltype(n)>(n) % 2 == 0;  // 没有意义
});

// ✅ 简单直接:直接使用 n
auto filtered = nums | std::views::filter([](auto&& n) {
    return n % 2 == 0;
});

何时使用 std::forward

// ✅ 需要转发时使用(如在 transform 中构造对象)
struct Widget {
    Widget(int x) : value(x) {}
    Widget(const Widget&) = delete;  // 不可拷贝
    Widget(Widget&&) = default;      // 只能移动
    int value;
};

std::vector<Widget> widgets = {Widget(1), Widget(2), Widget(3)};

// 使用 std::forward 保持移动语义
auto transformed = widgets | std::views::transform([](auto&& w) {
    return Widget(std::forward<decltype(w)>(w).value * 2);
});

实战指南:何时使用哪种参数类型

决策树:

是否需要修改元素?
├─ 是 → 使用 auto&&(谨慎使用)
└─ 否 → 是否需要处理代理对象?
    ├─ 是 → 使用 auto&&
    └─ 否 → 元素类型是否小型可拷贝?
        ├─ 是 → 使用 auto(简单)
        └─ 否 → 使用 const auto&

具体场景示例:

// 场景 1:简单内置类型,只读
auto result = nums | std::views::filter([](auto n) {  // auto 足够
    return n > 0;
});

// 场景 2:大型对象,只读
struct BigData {
    std::vector<int> data;
    // ... 大量数据
};

std::vector<BigData> items;
auto result = items | std::views::filter([](const auto& item) {  // const auto& 避免拷贝
    return !item.data.empty();
});

// 场景 3:处理 vector<bool> 的代理对象
std::vector<bool> flags = {true, false, true};
auto result = flags | std::views::filter([](auto&& flag) {  // 必须用 auto&&
    return flag;  // flag 是 vector<bool>::reference(代理对象)
});

// 场景 4:zip 返回的 tuple 引用
std::vector<int> a = {1, 2, 3};
std::vector<std::string> b = {"a", "b", "c"};

auto zipped = std::views::zip(a, b);
auto result = zipped | std::views::filter([](auto&& pair) {  // 必须用 auto&&
    auto& [num, str] = pair;  // pair 是 tuple<int&, std::string&>
    return num > 1;
});

// 场景 5:transform 中需要转发移动语义
auto result = widgets | std::views::transform([](auto&& w) {
    return std::forward<decltype(w)>(w).value * 2;  // 保持移动语义
});

性能对比:不同参数类型的开销

#include <benchmark/benchmark.h>
#include <vector>
#include <ranges>

struct ExpensiveObject {
    std::vector<int> data;
    int key;
    ExpensiveObject() : data(1000), key(0) {}
};

class BenchmarkFixture : public benchmark::Fixture {
public:
    void SetUp(const benchmark::State& state) override {
        objects.resize(10000);
        for (int i = 0; i < 10000; ++i) {
            objects[i].key = i;
        }
    }

    void TearDown(const benchmark::State& state) override {
        objects.clear();
        objects.shrink_to_fit();
    }

    std::vector<ExpensiveObject> objects;
};

BENCHMARK_DEFINE_F(BenchmarkFixture, BM_Filter_AutoValue)(benchmark::State& state) {
    for (auto _ : state) {
        auto r = objects | std::views::filter([](auto obj) {
            return obj.key % 2 == 0;
        });
        int count = 0;
        for (auto& obj : r) count++;
        benchmark::DoNotOptimize(count);
    }
}
BENCHMARK_REGISTER_F(BenchmarkFixture, BM_Filter_AutoValue);

BENCHMARK_DEFINE_F(BenchmarkFixture, BM_Filter_ConstAutoRef)(benchmark::State& state) {
    for (auto _ : state) {
        auto r = objects | std::views::filter([](const auto& obj) {
            return obj.key % 2 == 0;
        });
        int count = 0;
        for (auto& obj : r) count++;
        benchmark::DoNotOptimize(count);
    }
}
BENCHMARK_REGISTER_F(BenchmarkFixture, BM_Filter_ConstAutoRef);

BENCHMARK_DEFINE_F(BenchmarkFixture, BM_Filter_AutoForwardRef)(benchmark::State& state) {
    for (auto _ : state) {
        auto r = objects | std::views::filter([](auto&& obj) {
            return obj.key % 2 == 0;
        });
        int count = 0;
        for (auto& obj : r) count++;
        benchmark::DoNotOptimize(count);
    }
}
BENCHMARK_REGISTER_F(BenchmarkFixture, BM_Filter_AutoForwardRef);

BENCHMARK_MAIN();

运行结果:

BenchmarkTimeCPUIterations
BenchmarkFixture/BM_Filter_AutoValue977061 ns971965 ns715
BenchmarkFixture/BM_Filter_ConstAutoRef2837 ns2827 ns247517
BenchmarkFixture/BM_Filter_AutoForwardRef2854 ns2844 ns246198
  • auto:最慢(每次调用都拷贝整个对象)
  • const auto&:快(零拷贝)
  • auto&&:与 const auto& 相当(零拷贝,但更通用)

高级技巧:完美转发在 Ranges 中的应用

技巧 1:在 transform 中完美转发

// 创建对象的工厂函数
template<typename T>
auto makeObject(T&& arg) {
    return Object(std::forward<T>(arg));
}

// 在 transform 中使用
auto result = inputs | std::views::transform([](auto&& input) {
    return makeObject(std::forward<decltype(input)>(input));
});

技巧 2:处理可变参数

// 使用 zip 和 tuple 解构
auto pairs = std::views::zip(vec1, vec2);
auto result = pairs | std::views::transform([](auto&& pair) {
    auto& [a, b] = pair;
    return std::forward<decltype(a)>(a) + std::forward<decltype(b)>(b);
});

技巧 3:条件性转发

// 根据条件决定是否转发
auto result = items | std::views::transform([](auto&& item) {
    if (shouldCopy(item)) {
        return item;  // 拷贝
    } else {
        return std::move(item);  // 移动
    }
});

总结:auto&& 最佳实践清单

✅ 推荐使用 auto&& 的场景:

  1. Ranges adaptor 的 lambda 参数(默认首选)
  2. 处理代理对象(vector<bool>ziptransform 等)
  3. 需要保持值类别进行完美转发
  4. 不确定元素类型或值类别时

❌ 避免使用 auto&& 的场景:

  1. 只需要简单读取且元素类型小型可拷贝(用 auto
  2. 只需要读取且确定是普通对象(用 const auto&
  3. 不需要转发语义(避免过度使用 std::forward

⚠️ 注意事项:

  1. auto&& 允许修改,确保不会意外修改原始数据
  2. 注意视图的生命周期,避免悬空引用
  3. filter 中通常不需要 std::forward
  4. transform 中需要转发时才使用 std::forward

🎯 黄金法则:

在 Ranges adaptor 中,默认使用 auto&&。只有在明确知道元素类型且不需要处理代理对象时,才考虑使用 const auto&auto

按结构体成员排序(投影)

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

struct Person {
    std::string name;
    int age;
};

int main() {
    std::vector<Person> people = {
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35}
    };
    
    // 传统方式:需要写 lambda
    std::sort(people.begin(), people.end(),
        [](const Person& a, const Person& b) { return a.age < b.age; });
    
    // Ranges 方式:使用投影
    std::ranges::sort(people, {}, &Person::age);
    
    for (const auto& p : people) {
        std::cout << p.name << " (" << p.age << ") ";  // Bob (25) Alice (30) Charlie (35)
    }
}

投影的威力

  • &Person::age 告诉算法按哪个成员比较
  • 空花括号 {} 使用默认比较(升序)
  • 适用于所有 Ranges 算法:findmax_element

std::views::split 的深度解析:char vs string 陷阱

std::views::split 是最常用的适配器之一,但它也是陷阱最多的地方,尤其是当你混用字符和字符串作为分隔符时。

1. 字符串字面量的“隐形”陷阱

这是最经典的坑。如果你写 split(" "),你可能以为是在用空格分割,但实际上:

std::string text{"hello world"};

// ❌ 错误:分隔符被推导为 const char[2],包含 {' ', '\0'}
auto parts{text | std::views::split(" ")}; 

由于 " " 是一个 C 风格字符串(数组),它包含一个隐藏的空终止符 \0views::split 会将其视为一个要匹配的范围

正确做法

// ✅ 使用字符:明确分割单个元素
auto parts{text | std::views::split(' ')}; 

// ✅ 使用 string_view:明确分割范围且不包含 \0
using namespace std::literals;
auto parts{text | std::views::split(" "sv)}; 

2. C++20 vs C++23:splitlazy_split

在 C++20 中,views::split 的行为其实是“懒惰”的,这导致了性能问题。C++23 对此进行了重构:

  • std::views::lazy_split (C++20 的旧行为):
    • 适用于输入范围 (Input Range),如从文件流读取。
    • 内部迭代器比较复杂,性能较低。
    • 即使输入是连续内存(如 string),生成的子范围也不是 contiguous_range
  • std::views::split (C++23 新行为):
    • 要求前向范围 (Forward Range)
    • 性能更好,生成的子范围保留了原容器的特性(如 contiguous_range),可以直接构造 string_view

3. 返回类型的差异

当你使用 char 作为分隔符时,split 的实现通常更简单高效。

std::string s{"a,b,c"};
auto r{s | std::views::split(',')};

// 在 C++23 中,r 的元素(子范围)可以直接转换为 string_view
for (auto&& part : r) {
  std::println("{}", std::string_view{part});
}

4. 性能建议

  • 优先使用 char:如果分隔符只是单个字符,永远优先使用 ' ' 而不是 " "
  • 避免重复转换:如果你需要将结果转换回 string,使用 C++23 的 std::ranges::to
auto vec{s | std::views::split(',') | std::ranges::to<std::vector<std::string>>()};

收集结果到容器(C++23)

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

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6};
    
    // C++20 方式:需要手动收集
    std::vector<int> evens;
    for (int n : nums | std::views::filter([](int n) { return n % 2 == 0; })) {
        evens.push_back(n);
    }
    
    // C++23 方式:使用 to
    auto evens = nums
        | std::views::filter([](int n) { return n % 2 == 0; })
        | std::ranges::to<std::vector>();
    
    for (int n : evens) {
        std::cout << n << " ";  // 输出: 2 4 6
    }
}

常用 Adaptors 速查表

Adaptor功能示例结果
filter过滤元素nums | filter(even)只保留偶数
transform转换元素nums | transform(square)每个元素平方
take取前 N 个nums | take(3)前 3 个元素
drop跳过前 N 个nums | drop(2)跳过前 2 个
reverse反转nums | reverse反序遍历
keys提取 map 键map | keys所有键
values提取 map 值map | values所有值
iota生成序列iota(1, 10)1,2,3,…,9
zip合并范围zip(vec1, vec2)元组对

实用组合示例

// 1. 取前 10 个正偶数
auto result = std::views::iota(1)
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::take(10);

// 2. 反转并跳过前 3 个
auto result = nums
    | std::views::reverse
    | std::views::drop(3);

// 3. 提取 map 的值并排序
std::map<int, std::string> m = {{3, "c"}, {1, "a"}, {2, "b"}};
auto sorted_values = m
    | std::views::values
    | std::views::transform([](const std::string& s) { return s; });
// 注意:map 的 values 视图已经是排序的

// 4. 处理嵌套容器
std::vector<std::vector<int>> nested = {{1, 2}, {3, 4}, {5, 6}};
auto flat = nested | std::views::join;  // 1,2,3,4,5,6

Ranges Algorithms vs 传统 STL

对比表

操作传统 STLRanges优势
排序sort(vec.begin(), vec.end())ranges::sort(vec)更简洁
查找find(vec.begin(), vec.end(), 42)ranges::find(vec, 42)直接传容器
按成员查找find_if(..., [](auto& x) {
return x.id==42;
})
ranges::find(vec, 42, &X::id)投影简化
复制copy(vec.begin(), vec.end(), ...)ranges::copy(vec, ...)无需迭代器对

实际案例对比

案例 1:查找结构体成员

struct Item {
    int id;
    std::string name;
};

std::vector<Item> items = {{1, "apple"}, {2, "banana"}, {3, "cherry"}};

// 传统 STL:需要 lambda
auto it = std::find_if(items.begin(), items.end(),
    [](const Item& i) { return i.id == 2; });

// Ranges:使用投影
auto it = std::ranges::find(items, 2, &Item::id);

案例 2:复杂管道操作

std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// 传统 STL:需要多个中间容器
std::vector<int> temp1, temp2, result;
std::copy_if(vec.begin(), vec.end(), std::back_inserter(temp1),
    [](int n) { return n % 2 == 0; });
std::transform(temp1.begin(), temp1.end(), std::back_inserter(temp2),
    [](int n) { return n * n; });
std::copy_n(temp2.begin(), 3, std::back_inserter(result));

// Ranges:一行搞定
auto result = vec
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::transform([](int n) { return n * n; })
    | std::views::take(3)
    | std::ranges::to<std::vector>();

懒惰求值的威力

什么是懒惰求值?

视图不会立即执行操作,而是在迭代时才计算:

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

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5};
    
    // 创建视图(不执行任何计算)
    auto filtered = nums
        | std::views::filter([](int n) {
            std::cout << "filter: " << n << "\n";
            return n % 2 == 0;
        })
        | std::views::transform([](int n) {
            std::cout << "transform: " << n << "\n";
            return n * n;
        });
    
    std::cout << "开始迭代...\n";
    
    // 只在迭代时计算
    for (int n : filtered) {
        std::cout << "结果: " << n << "\n";
    }
}

输出

开始迭代...
filter: 1
filter: 2
transform: 2
结果: 4
filter: 3
filter: 4
transform: 4
结果: 16
filter: 5

懒惰求值的优势

  1. 性能优化:只计算需要的元素
  2. 无限序列:可以处理无限数据流
  3. 组合灵活:视图可以无限组合而不产生中间结果
// 无限序列示例
auto squares = std::views::iota(1)
    | std::views::transform([](int n) { return n * n; });

// 只取前 5 个平方数
for (int sq : squares | std::views::take(5)) {
    std::cout << sq << " ";  // 1 4 9 16 25
}

迭代器概念与限制

Ranges 迭代器层次

概念能力典型容器支持的操作
input_iterator单次读取istream_iterator++it, *it
forward_iterator多次读取forward_list, unordered_map++it, 多次 *it
bidirectional_iterator双向移动list, map, set++it, --it
random_access_iterator随机访问vector, deque, arrayit + n, it[n]
contiguous_iterator连续内存vector, array, string_view&*it 得到地址

Adaptor 对迭代器的要求

Adaptor最低要求原因
filterforward_range需要多次检查元素
transforminput_range只需单次读取
reversebidirectional_range需要回退
take / dropinput_range只需单次遍历
joininput_range + 解引用返回 range需要能解引用出子范围
splitforward_range需要保存分隔符位置

常见陷阱

陷阱 1:forward_list 不能用 reverse

std::forward_list<int> list = {1, 2, 3};

// ❌ 编译错误:需要 bidirectional_iterator
// auto reversed = list | std::views::reverse;

// ✅ 解决:转换为支持反向的容器
auto vec = std::vector(list.begin(), list.end());
auto reversed = vec | std::views::reverse;

陷阱 2:悬空迭代器保护

// 传统 STL:危险
auto it = std::find(std::vector{1, 2, 3}.begin(),
                    std::vector{1, 2, 3}.end(),
                    42);
// *it 悬空!临时 vector 已销毁

// Ranges:安全
auto it = std::ranges::find(std::vector{1, 2, 3}, 42);
// it 是 std::ranges::dangling,无法解引用

C++23 新特性

std::ranges::to - 收集结果

#include <ranges>
#include <vector>
#include <map>

int main() {
    std::vector<std::pair<int, std::string>> pairs = {{1, "a"}, {2, "b"}};
    
    // 转换为 map
    auto m = pairs | std::ranges::to<std::map>();
    
    // 转换为 vector
    auto v = pairs | std::ranges::to<std::vector>();
}

std::ranges::contains - 检查包含

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

// 传统方式
bool found = std::find(vec.begin(), vec.end(), 3) != vec.end();

// Ranges 方式
bool found = std::ranges::contains(vec, 3);  // 更清晰

std::views::zip - 合并范围

std::vector<std::string> keys = {"a", "b", "c"};
std::vector<int> values = {1, 2, 3};

// 合并为元组对
auto zipped = std::views::zip(keys, values);

for (const auto& [k, v] : zipped) {
    std::cout << k << ": " << v << " ";  // a: 1 b: 2 c: 3
}

// 转换为 map
auto map = zipped | std::ranges::to<std::map>();

std::views::chunk - 分块

std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9};

// 每 3 个元素一组
auto chunks = vec | std::views::chunk(3);

for (auto chunk : chunks) {
    for (int n : chunk) {
        std::cout << n << " ";
    }
    std::cout << "\n";
}
// 输出:
// 1 2 3
// 4 5 6
// 7 8 9

实战案例

案例 1:处理 CSV 数据

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

struct Record {
    std::string name;
    int age;
    double score;
};

int main() {
    std::string csv = "Alice,30,95.5\nBob,25,87.0\nCharlie,35,92.3";
    std::istringstream iss(csv);
    
    auto records = std::views::istream<std::string>(iss)
        | std::views::transform([](const std::string& line) {
            std::istringstream line_ss(line);
            std::string name, age_str, score_str;
            std::getline(line_ss, name, ',');
            std::getline(line_ss, age_str, ',');
            std::getline(line_ss, score_str, ',');
            return Record{name, std::stoi(age_str), std::stod(score_str)};
        });
    
    // 过滤分数 > 90 的记录
    auto high_scorers = records
        | std::views::filter([](const Record& r) { return r.score > 90; });
    
    for (const auto& r : high_scorers) {
        std::cout << r.name << ": " << r.score << "\n";
    }
}

案例 2:滑动窗口计算移动平均

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

int main() {
    std::vector<double> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int window_size = 3;
    
    // 使用 C++23 的 slide
    auto windows = data | std::views::slide(window_size);
    
    for (auto window : windows) {
        double sum = 0;
        for (double val : window) {
            sum += val;
        }
        std::cout << "平均: " << sum / window_size << "\n";
    }
    // 输出: 2, 3, 4, 5, 6, 7, 8, 9
}

案例 3:生成斐波那契数列

#include <iostream>
#include <ranges>
#include <generator>  // C++23

std::generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        std::tie(a, b) = std::make_tuple(b, a + b);
    }
}

int main() {
    // 取前 10 个斐波那契数
    auto fibs = fibonacci() | std::views::take(10);
    
    for (int f : fibs) {
        std::cout << f << " ";  // 0 1 1 2 3 5 8 13 21 34
    }
}

最佳实践

✅ DO - 推荐做法

// 1. 优先使用 Ranges 算法
std::ranges::sort(vec);
std::ranges::find(vec, value);

// 2. 使用投影简化代码
std::ranges::sort(people, {}, &Person::age);

// 3. 管道式组合视图,并使用 auto&& 接收参数
auto result = vec
    | std::views::filter([](auto&& n) { return n > 0; })
    | std::views::transform([](auto&& n) { return n * 2; })
    | std::ranges::to<std::vector>();

// 4. 使用 contains 而不是 find
if (std::ranges::contains(vec, value)) { ... }

// 5. 立即收集结果避免悬空迭代器
auto result = view | std::ranges::to<std::vector>();

❌ DON’T - 避免做法

// 1. 不要对临时容器使用算法返回迭代器
auto it = std::ranges::find(std::vector{1, 2, 3}, 2);  // it 是 dangling

// 2. 不要对不支持的操作使用视图
std::forward_list<int> list = {1, 2, 3};
auto reversed = list | std::views::reverse;  // 编译错误

// 3. 不要忘记视图是懒惰的
auto filtered = vec | std::views::filter(pred);
// filtered 不会立即执行,直到迭代

// 4. 不要过度组合导致性能问题
auto complex = vec
    | std::views::filter(...)
    | std::views::transform(...)
    | std::views::filter(...)
    | std::views::transform(...);  // 考虑是否需要中间容器

性能考虑

零成本抽象

Ranges 的性能通常与手写循环相当:

// Ranges 版本
auto result = vec
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::transform([](int n) { return n * n; });

// 编译器优化后,等价于
for (int n : vec) {
    if (n % 2 == 0) {
        // 处理 n * n
    }
}

何时使用容器收集

// ✅ 适合使用视图的场景
for (int n : vec | std::views::filter(pred)) {
    // 单次遍历,不需要中间结果
}

// ✅ 需要多次遍历时收集到容器
auto filtered = vec
    | std::views::filter(pred)
    | std::ranges::to<std::vector>();

// 现在可以多次遍历 filtered
for (int n : filtered) { ... }
for (int n : filtered) { ... }

总结

Ranges 的核心价值

  1. 可读性:声明式代码,意图清晰
  2. 组合性:管道操作,灵活组合
  3. 性能:懒惰求值,零成本抽象
  4. 安全性:编译期检查,防止悬空迭代器
  5. 简洁性:投影简化,减少样板代码