惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
U
Unit 42
人人都是产品经理
人人都是产品经理
罗磊的独立博客
Recent Announcements
Recent Announcements
云风的 BLOG
云风的 BLOG
aimingoo的专栏
aimingoo的专栏
T
Tailwind CSS Blog
GbyAI
GbyAI
Blog — PlanetScale
Blog — PlanetScale
I
InfoQ
Last Week in AI
Last Week in AI
宝玉的分享
宝玉的分享
B
Blog RSS Feed
WordPress大学
WordPress大学
腾讯CDC
H
Help Net Security
博客园 - Franky
博客园 - 【当耐特】
博客园 - 聂微东
Stack Overflow Blog
Stack Overflow Blog
B
Blog
Vercel News
Vercel News
博客园 - 司徒正美

物灵

A non-gamer’s view on games ◎物灵 I am a ruin aborigine ◎ 物灵 The spreadsheet-makers ◎物灵 وِسَ ڠ دَاِقࣨ؟ ◎物灵 Do not get gentle into your CS major ◎物灵 ཡིའུ་ཀང་ལག་ ◎ 物灵 슈홋(守候) 2038넌(年) ◎物灵 Buku-name de Mini kon Sina-litera ◎物灵 ‘My Path’ after twenty years ◎物灵 My linguistic history ◎物灵 A slice of Bash programmable completion ◎物灵 Enhypen Alphabet ◎物灵 GNU 四十年 ◎物灵 ‘If need be, I will leave w/o looking back…’ ◎物灵 omekapo, Lánzhōu ⟲物灵 说说选登 ◎ 物灵 Back in old Ruyqiuan ◎物灵 linluwi lili li pona a! ⟲物灵 「挺」的中古三字 ◎物灵 最后一天 ◎物灵 基于 RFID 及智能终端的户外团队运动系统 ◎物灵 Which character for inbound passengers? ◎物灵 Kokanu in lika Hanci ◎物灵 Number of shapes of Rubik’s snake ◎物灵 دࣱخُاْ ۋاًخُوَاتࣨ ◎物灵 Childhood ◎物灵 个人视觉识别系统(辛丑) ◎物灵 ((报告 书) = (书 (报告 书)) ◎物灵 Speech before final examination ◎物灵 第 34 届山西科创记 ◎物灵
领有和非领有的 C++ range ◎物灵
Unknown · 2025-09-13 · via 物灵

这是讨论 C++ range 及其 range adaptor 等底层性质系列文章之第一篇。同时,我介绍了意在解决本文所述一些问题的实验性库之设计。

前言

C++ range 是 C++20 最紧要特性之一,详参我之前的文章。我很喜欢 C++ range,在多个项目(主要是生物信息学、数据科学等)大量使用,但我难以说服他人了解其优势。我见到许多程序员对其基本使用仍有困难。自 C++ 初版发布后 ISO 标沝 range 部分又有诸多理念变化,非专业用户必更难理解这些动向。ISO C++ 委员会内部对 C++ range 和 view 至今仍有争议讨论。

我看到当前设计的几个问题,我会逐一撰写博文:

  1. 领有/owning 和非领有的 range
  2. 待定
  3. ……

我会使文章信息量刚刚好,但也提供足够信息了解这一切喧嚣。最后我将介绍我自己能解决一些问题的实验库设计。这是系列的第一篇文章。

免责声明:我使用 range 已有段时间,但我明白是其他人将 range 带入标沝。很可能有我未虑及之事,我也认为应当尝试且改进 view 之设计。我对几个问题已多次改变主意,且相信若无前几次设计迭代,我绝无可能得到这些结论。或者借用一位智者的话:

你为什么离开?这样你才能归来。这样你看着来处,才有新的视野与额外的色泽。〔中略〕回到开始的地方不等于从未离开。

— Terry Pratchett, A Hat Full of Sky

C++17 标沝库

我们从 C++20 以前标沝库的 range 开始,毕竟这是人们最熟知的。注意虽然 range 本身始于 C++17,我将用到后起术语/概念/算法解释彼此关系。记住 C++ 中类型成为 range 只要求 begin()end()。其馀都是额外的好处 😉。

多趟领有 range(容器)

即令没有 range,容器也天然是 range。容器领有/own 自己的元素,即元素存储由容器管理,容器消失时元素便消失。容器是多趟 range,即可以多次迭代,每次看到同样的元素。

用线绑的实体字串
字串是容器之一例

图上是一种想象容器的方式,一串元素(字符)及控制用数据结构(图上是线和夹子)。有 begin(首个字符)和 end(线头)。重要的是元素依附于控制数据结构,若移动,元素与容器一併移动,若抽掉线,则字符消失。

/* 构造与複製 */
std::string s  = "foobar";      // 複製所有字符自静态区至 string [O(n)]
std::string s2 = s;             // 複製所有字符至 s2              [O(n)]

/* 从字串取最小元素 */
char c = *std::ranges::min_element(s);
assert(c == 'a');

/* 试图从临时量取最小元素 */
// char d = *std::ranges::min_element(std::string{"foobar"});
// assert(d == 'a');
展示容器语义的代码示例

虽然 "foobar" 字串字面量属静态存储,构造分配了新字串1,将字符複製其中。容器管理其元素之内存。复制容器时复制元素(O(n) 时间)。

容器上可以调用 std::ranges::min_element,取得指向字典序最小字符的迭代器。字串临时量则不可,因其元素在 min_element 返回时超出作用域,故返回的迭代器悬垂。2

多趟非领有 range

如果容器是领有的 range,那什么是非领有的?C++17 引入第一个例子:std::string_view。一个仅含另一个 range 首末指针3 的 range。

一般而言,非领有 range 是一类不管理其元素内存的 range。std::string_view 便是如此,甚至达到了借用 range 的更严要求:该种 range 内只有迭代器,且迭代器不指向 range 本身。个中一个重要含义是当借用 range 自身超出作用域时其迭代器仍有效。这对 std::string_view 似乎当然,但稍后我们会遇到需要权衡的设计。

容器在墙上投影(注意悬空的逗号!)
如果字串是容器,string_view 象是投下的影子。

上图展示了 string_view 作为字串的影子:影子能读出相同的字母,並观察到类似字串本身的属性(我的例子中除了颜色 😄)。但影子的存期取决于被投影对象,如果原形移动或销毁,影子也消失,string_view 变成悬垂指针。

/* 构造与複製 */
std::string_view s = "foobar";      // "绑定" s 至静态存区   [O(1)]
std::string_view s2 = s;             // "绑定" s2 至静态存区  [O(1)]

/* 从字串取最小元素 */
char c = *std::ranges::min_element(s);
assert(c == 'a');

/* 从临时对象取最小元素 */
char d = *std::ranges::min_element(std::string_view{"foobar"});
assert(d == 'a');
展示借用 range 语义的代码示例

单趟 range

单趟 range 不保证可多次遍历,或者褈複遍历时元素可能不同。迭代这种 range 就改变 range。甚至不保证能多次调用 .begin() 方法。

打印机
单趟字符 range 可类比为打印机正在打印字母

概念上,单趟 range 比起元素集合更象「元素生成器」,上图打印机正是意在于此。可以生成一系列䈎面,但䈎面不属于打印机自身,且打印一面后打印机状态会改变。打印机过程可能无限持续,或突然停止。有些打印机能褈启,有些不能 😁

C++20 之前没有专门的单趟 range,也少有单趟迭代器。std::istream_iterator 是一例,暴露了输入流的字符。

单趟 range 与领有和非领有的关系复杂:

  • 通常是只移动类型,不能复制(不是 O(1) 也不是 O(n))。
  • 元素无所谓领有与不领有,因为通常被立即使用且消耗,指向先前元素的迭代器会失效。
  • 从单趟 range 借用没有意义,因为只能有一个有效的迭代器。4

因此我建议将单趟 range 作为独立类别保留,不在本文继续讨论。

C++2x 标沝库

C++20 是 range 真正落地的版本,整个机制包括概念与算法成为标沝库之一部分。但引入的重要新特性是 range adaptor,range adaptor 是在其他 range 创建的 range5,通常暴露原 range 的子集或转换。有关介绍请见前文

std::vector vec{1, 2, 3, 4};
auto v = vec | std::views::VIEW1 | std::views::VIEW2;
// 非领有多趟 range adaptor

这是典型「管道」的形状。其声明 v,这是 vec 上的 range adaptor,经过 VIEW1VIEW2 表示的转换。std::string_view 主要用于字串字面量和子串,而现在允许的操作强大得多,包括

  • filter(如:㝻非空格字符)
  • 转换(如:转换成大写)
  • split 和 join(如:以空格拆分、用分隔符连接)

许多 adaptor 有状态,如 filter 需要存储用于过滤的谓词,join 需要分隔符。另有一些 adaptor 有缓存。状态可以存于 adaptor,或存于迭代器,前者为迭代器节省空间,但使 adaptor 不能成为借用 range(迭代器需要存储指向 adaptor 的指针以访问状态)。

range-v3 与原版 C++20 的视图

标沝库 range 设计基于 Eric Niebler 的 range-v3 库,该库推广了「视图」这一术语。

range-v3 的视图定义为 O(1) 可複製的 range。这个「非领有」定义较借用 range 广泛,视图可以保留状态,只要是 O(1)。因此借用 range 是视图的子集6

我在下面讨论这一灵活性是否值得,但要注意,至今视图的定义依然清晰,主要围绕所有权:它意谓着「O(1) 可複製」!然而须知在这一点上视图已不等于 range adaptor。range-v3 与早期 C++20 已有一些自立的 range 亦称为视图,如 std::ranges::iota_viewstd::ranges::empty_view(亦可在常量时间复制)。

P2415 之后(当前状态)

C++20 正式发布前,委员会通过了 P1456,允许视图成为只移动类型。但 C++20 发布后,P2325 作为阙陷报告用于 C++20,允许视图不要求可默认构造。我将在下篇博文探讨这两点。

然而最重要也最具争议的改动在 P2415,允许视图成为领有 range。该提案亦作为阙陷报告譍用于 C++20,而这是相当重大的设计变更。

auto to_upper = [] (unsigned char c) { return std::toupper(c); };
std::string s = "foobar";

/* 这原本便可行; v_indiri 依赖 s */
auto v_indiri = s | std::views::transform(to_upper);

/* 这是 "新增" 的; v_owning 自立 */
auto v_owning = std::move(s) | std::views::transform(to_upper);

译注:还有一些问题后续修改才解决。)

这是一个有用特性,然而导致 std::ranges::view 概念发生变化,不再表示非领有 range。侭管该概念与其他概念联用时仍有些价值,但我认为其在标沝库的视图机制外已然无用。尤其是它不能用于约束自己的算法,而这是概念的初衷。7

除了这些(不)实用的影响,还有严重的不明悫性(不可教性)。由于之前的定义广汎传播(且令人难忘),我曾遇到有经验的 C++ 程序员甚至委员会成员讶异于视图不一定是非领有 range。虑及「视图」是 C++ range 最大卖点之一,无法解释「视图」一词之含义是大问题。

鉴于「修复」定义为时已晚,且再次改变会更加引起混乱,我建议完全避免使用「视图」一词

  • 用 range adaptor 描述操作(包装)其他 range 的 range。
  • 用 range adaptor 对象描述在管道链式调用以创建 range adaptor 的对象8
  • 用非领有 range 表示 O(1) 可複製的 range。
  • 用借用 range 表示 O(1) 可複製,且迭代器可超出 range 存期的 range。

RADR 库

📡
https://github.com/h-2/radr

我刚才公开了我自己的实验性范围库,名为 ℝange 𝔸𝕕aptors ℝeimagined(radr)。

其基于 C++20 概念与算法,但替换了 std::views::*,且提供相似的用法模式。

请查看且提供反馈!我在本文只会涵盖关于所有权的设计选择,但此库还解决了其他问题,后续会有更多博文。GitHub 仓库的 docs 文件夹有详尽文档。

非领有 range adaptor

radr 中一切非领有的(多趟)range adaptor 均是借用 range。亦即所有功能以迭代器哨兵对实现,range 没有其他状态。此举代价是迭代器增大,但能显著降低整体复杂度,且为用户提供更简洁的思维模型:

  • 只有一种「非领有 range」,不是两种。view 概念与本库完全无关。
  • 「借用 range」既符合强定义,也符合实用的「非领有 range」定义。
  • range 管道输入借用 range,结果总是借用 range(对 std:: 不然)。

虽然我认为「借用 range」甚至优于旧的 range-v3 视图概念,避开导致混淆的新视图概念才是开发 radr 的主要动机。

/* 寻找 vector 之最小非负数 */
std::vector vec{-1, 2, -3, 1, 7};
auto non_neg = [] (int i) { return i >= 0; };

auto it1 = std::ranges::min_element(vec | std::views::filter(non_neg));
// assert(*it == 1);   // 不可用,因 filter_view 未借用

auto it2 = std::ranges::min_element(std::ref(vec) | radr::filter(non_neg));
assert(*it == 1);      // 可用,因所有 radr adaptor 是借用
此例显示了该设计选择之一个实际优势

创建间接性的语法在 radr 是显式的(使用 std::ref(),详后),但此外其使用方法(如管道)与标沝库完全一致。

关键区别是返回 range 始终是借用 range,因此可以可靠重用迭代器。这类似 Boost-v2 对 adaptor 的设计。标沝库中一些 adaptor 返回借用 range,但许多重要操作如 filter 非然。用户难以判断特定 adaptor 是否如此。9

总体而言,radr 尝试仿照 std::string_view 行为以处理非领有 range adaptor。该机制自 C++17 起便存在,为众熟知。

领有 range adaptor

虽然 P2415 常因引入「领有视图」受到批评,我认为领有 range adaptor 並非问题所在(且很有用!)。问题是「视图」的定义被扩充至包含领有 range adaptor,因而剥离其核心本质:非领有的 range。10

所有故制中,管理其元素内存的 range 即为容器。包装元素右值故而管理其内存的 range adaptor 同样是容器,因其含有元素!不需要引入新概念。Range 默认意义下元素可有任意所有权,惟一需要明悫建模的是 range 是否领有与是否借用。

std::

/* 在临时量创建 adaptor */
auto vue0 = std::vector{1, 2, 3} | std::views::take(2);
/* 移动已有容器 */
std::vector vec{1, 2, 3};
auto vue1 = std::move(vec) | std::views::take(2);

radr::

/* 在临时量创建 adaptor */
auto rad0 = std::vector{1, 2, 3} | radr::take(2);
/* 移动已有容器 */
std::vector vec{1, 2, 3};
auto rad1 = std::move(vec) | radr::take(2);

对容器右值包装 radr 库与标沝库可实现相同功能,语法完全一致。但 radr 视返回 range 为容器,可进行 O(n) 複製

总结

存在领有 range 和非领有 range,借用 range 是后者的严格版本。过去「视图」曾与非领有 range 同义,但视图概念经历多次新定义后已失去实用性。描述 range 最好采用其他明悫无误的术语。另外,range adaptor 这一术语指在其他 range 上操作所得 range。该概念与所有权正交,即 range adaptor 可以指向底层 range,亦可进行封装。

由此可提炼两个关键设计问题:

  • 非领有 range adaptor 如何实现?
    • 它们是否全部/部分/完全借用?
  • 是否需要领有 range adaptor?若需,如何建模?

标沝库类似 range-v3 类似,选择部分非领有 range adaptor 采取借用,但多数并非如此。RADR 库类似 Boost range-v2,选择所有非领有 range adaptor 是借用范围。

标沝库与 RADR 提供领有 range adaptor,Boost range-v2 与 range-v3 则无。标沝库中领有 range adaptor 是一种视图,RADR 中则只是容器。

我已尽力将这个复杂话题分解阐述,但我希望已经说明:「视图」难以解释正是问题所在。radr 库提供十分类似 std::views 的功能,同时试图规避部分复杂性。

:表示在下篇博文详叙。

译自 Hannes Hauswedell查看原文。授权方式:
© unless otherwise noted: text cc-by-sa, code cc0, photos gplv3。