下面分为常见面试题和常见坑两部分。代码都没有实测,涉及标准细节的地方以 C++20 最终版及其后续缺陷修正(DR)为准。

一、常见面试题

1. ranges 是什么,解决了什么问题

ranges 包含三部分:

  • range:任何能调用 begin()/end() 的东西。end 可以和 begin不同的类型,这种 end 叫 sentinel(哨兵)。
  • std::ranges:: 算法:直接接收整个 range,并且用 concept 约束参数。
  • views(视图):惰性求值、可以组合的范围适配器,用 | 串成管道。
std::vector<int> v{1, 2, 3, 4, 5, 6};
auto r = v | std::views::filter([](int x) { return x % 2 == 0; })
           | std::views::transform([](int x) { return x * x; });
for (int x : r) std::cout << x << ' ';   // 4 16 36;此前没有任何计算发生

C++20 的 ranges 来自 Eric Niebler 的 range-v3,标准只采纳了其中一个子集(P0896 "One Ranges Proposal")。

2. std::sortstd::ranges::sort 有什么区别

std::sort(first, last) std::ranges::sort(r)
参数 一对迭代器 整个 range,或迭代器加哨兵
约束 没有,传错了在模板内部报一大串错误 用 concept 约束,比如 sort(list) 直接报"不满足 random_access_range"
投影 不支持 ranges::sort(people, {}, &Person::age)
返回值 void 返回末尾迭代器
调用方式 普通函数模板,参与 ADL niebloid(函数对象),不参与 ADL,不能显式写模板参数
悬空保护 没有 对临时对象调用时返回 std::ranges::dangling

3. 投影(projection)是什么

投影是算法在比较或判断之前,先对每个元素调用的一个函数,省去了手写比较器:

std::ranges::sort(people, {}, &Person::age);            // 按 age 排序,{} 表示使用默认的 less
auto it = std::ranges::find(users, 42, &User::id);      // 找 id == 42 的用户
auto m  = std::ranges::max(people, {}, &Person::salary);

4. view 是什么,和容器有什么区别

  • view 不拥有元素,只引用底层的数据。C++20 的缺陷修正 P2415 引入了 owning_view,允许 view 持有一个右值容器。
  • view 的移动和析构都是 O(1)。所以 view 应该按值传递,不要按 const 引用传递,原因见坑 2。
  • 惰性求值:构造 view 时什么都不计算,只有迭代时才逐个计算,因此可以表示无限序列,比如 std::views::iota(1)
  • 每次迭代都重新计算:结果不会被缓存,只有 begin() 可能被缓存。

5. 什么是 sentinel,有什么用

哨兵让 end 可以是一个不同类型的"终止条件",不必事先算出结束位置:

struct NullTerm {
    friend bool operator==(const char* p, NullTerm) { return *p == '\0'; }
};
// 遍历 C 字符串时不用先调用 strlen
std::ranges::for_each(s, NullTerm{}, [](char c) { /*...*/ });

std::unreachable_sentinel 表示"永远到不了结尾",调用者保证一定能找到目标时用它,可以省掉每一步的边界检查。

6. 什么是 borrowed range 和 dangling

对一个右值 range 调用返回迭代器的算法,这个迭代器在语句结束时就会悬空。标准的做法是返回 std::ranges::dangling,任何使用它的操作都会编译失败:

auto it = std::ranges::find(get_vector(), 3);   // it 的类型是 std::ranges::dangling
// *it;                                          // 编译错误

borrowed range 是指迭代器不依赖 range 对象本身存活的类型,比如左值引用、std::string_viewstd::span,以及特化了 enable_borrowed_range 的类型。它们的右值版本仍然返回真正的迭代器。

7. 各版本加了什么

版本 内容
C++20 约束过的算法;filtertransformtakedroptake_whiledrop_whilereversejoinsplitiotakeysvalueselementscommonall
C++23 ranges::to(把 view 收集成容器)、zipenumeratechunkslidestrideadjacentjoin_withcartesian_productas_constranges::fold_leftstd::generator
C++26 views::concatviews::cache_latest 等(我的记忆,可能随最终版本变化)

C++20 最明显的缺口是没有 ranges::to,把 view 变回容器很麻烦(见坑 5)。

二、常见坑

坑 1:view 引用了已经销毁的数据

auto evens() {
    std::vector<int> v{1, 2, 3, 4};
    return v | std::views::filter([](int x) { return x % 2 == 0; });   // 引用了局部变量 v,悬空
}

get_vector() | views::filter(...) 这种直接对临时对象建 view 的写法是安全的:P2415 之后,右值容器会被移动进一个 owning_view危险的是 view 引用的左值先被销毁了,还有 lambda 按引用捕获了局部变量这两种情况。

坑 2:const 的 filter_view 不能迭代

filter_view::begin() 要找到第一个满足谓词的元素,这一步是 O(n)。标准要求 begin() 均摊 O(1),于是它第一次调用时把结果缓存下来,因此 begin() 不是 const 成员函数。

void print(const auto& r) { for (auto x : r) std::cout << x; }
print(v | std::views::filter(pred));   // 编译错误:const filter_view 没有 begin()

修法:参数改成 auto&& r,或者直接按值传 view,因为 view 很便宜。drop_whilechunk_bysplit 以及 reverse 作用于某些 view 时,也有同样的缓存问题。

坑 3:通过 filter 修改元素,让它不再满足谓词,是 UB

auto evens = v | std::views::filter([](int x) { return x % 2 == 0; });
for (int& x : evens) x += 1;     // 修改后元素变成奇数
for (int x : evens) { ... }      // 未定义行为:缓存的 begin 指向的元素已经不满足谓词

标准明确规定:可以通过 filter 的迭代器修改元素,但修改后的值如果不再满足谓词,行为未定义。同理,迭代过一次之后又修改底层容器(比如 push_back 导致重新分配),缓存的 begin 就失效了。

坑 4:先 transform 再 filter,转换函数被调用两次

auto r = v | std::views::transform(expensive)
           | std::views::filter([](auto y) { return y > 10; });
for (auto y : r) use(y);   // 每个被选中的元素,expensive 都调用了两次:filter 判断一次,你解引用时又一次

transform 不缓存结果,每次解引用都会重新计算。对策:

  • 调换顺序,先 filter 后 transform,前提是谓词能写在原始元素上。
  • expensive 足够便宜,并且没有副作用。
  • 在 C++26 里用 views::cache_latest

坑 5:不是所有 view 都是 common range,没法直接传给老接口

auto r = v | std::views::take_while(pred);          // begin 和 end 的类型不同
std::vector<int> out(r.begin(), r.end());           // 编译错误:vector 的构造函数要求两个迭代器类型相同

auto c = r | std::views::common;                    // C++20 的写法
std::vector<int> out2(c.begin(), c.end());
auto out3 = r | std::ranges::to<std::vector>();     // C++23 的写法

坑 6:views::iota(0, v.size()) 类型不匹配

for (auto i : std::views::iota(0, v.size())) {}      // 编译错误:int 和 size_t 不能组成一个 iota
for (auto i : std::views::iota(0uz, v.size())) {}    // C++23 引入 uz 后缀
for (auto i : std::views::iota(std::size_t{0}, v.size())) {}

坑 7:很多 view 没有 size()

filter_view 在迭代之前不知道有多少个元素,它不是 sized_range,调用 r.size() 会编译失败。std::ranges::distance(r) 可以用,但它会把整个 range 遍历一遍,是 O(n)。

坑 8:算法的返回值和老版本不同

auto [in, out] = std::ranges::copy(src, dst.begin());   // 返回 {in, out},不是只返回 out
auto tail = std::ranges::remove(v, 0);                   // 返回一个 subrange,表示要删除的尾部
v.erase(tail.begin(), tail.end());
std::erase(v, 0);                                         // C++20 更简单的写法

坑 9:niebloid 不能当普通函数模板用

  • 不能写显式模板参数:std::ranges::sort<...>(...) 写不出来。
  • ADL 找不到它们,要写全名 std::ranges::sort
  • 反过来说,它们本身是函数对象,可以直接作为参数传递:apply(std::ranges::sort)

坑 10:受约束的算法对迭代器类别有要求

  • std::ranges::sort(my_list) 编译失败,因为 std::list 不支持随机访问。要用 my_list.sort()
  • views::reverse 要求底层至少是双向的,所以 forward_list | views::reverse 编译失败。

坑 11:调试构建的性能和编译时间

  • 管道在 -O2 下通常能优化得和手写循环一样快。
  • -O0 下会有大量的函数调用层级,可能慢好几倍。
  • 模板很重,编译时间会增加;报错信息比以前好,但嵌套一深仍然很长。

坑 12:C++20 里缺少的功能

C++20 没有 zipenumerateto,也没有 range-v3 那种原地修改容器的 actions。想"带下标遍历",在 C++20 里只能用 iota 配合下标,或者自己写。

三、面试速答

  • ranges 和老算法的区别? 直接接收整个 range,用 concept 约束,支持投影,允许 end 是不同类型的哨兵,对临时 range 有悬空保护。
  • view 是什么? 不拥有元素、惰性求值、O(1) 移动的轻量 range,用 | 组合,应该按值传递。
  • 为什么 const 的 filter_view 不能迭代? begin() 会缓存第一个满足条件的位置,所以它不是 const 成员函数。
  • transform 接 filter 有什么问题? 转换函数会被调用两次,因为 transform 不缓存结果。
  • view 怎么变回 vector? C++23 用 ranges::to<std::vector>();C++20 先经过 views::common,再用迭代器对构造。
  • 什么是 dangling? 对右值 range 调用算法时返回的占位类型,一旦使用就编译报错,防止迭代器悬空。