截至C++23的情况似乎是(Godbolt):initializer_list
是一个连续的范围,但不是一个视图,也不是一个借用范围。这是令人费解的,因为实际上
我的假设是,当前的措辞主要是因为`initializer_list`实际上不是一个值类型;它更像是一个
语法标签,类似于`std::nullptr_t`或`std::reference_wrapper`。我们很少传递`initializer_list`类型的值。相反,它们会在调用点出现以表示语法特性。实际的类型本身并不像一个行为良好的C++20范围(如果这样的东西可以说存在的话)。
template<class R>
auto debone(R rg) {
static_assert(std::borrowed_range<R>);
return std::ranges::subrange(rg.begin(), rg.end());
}
auto x = debone<std::initializer_list<int>>({1,2,3,4});
如果这段代码没有编译失败,那么返回的 x 将包含一对迭代器,指向一个已经被销毁的临时数组(即 initializer_list 的支持数组)。
其他“仅参数类型”必须处理相同的权衡。它们通常会明确表明借用范围,并通过稍微难以意外从临时对象构造来证明自己的存在。
auto x = debone<std::string_view>("hello world");
// compiles quietly, but also happens to work
auto x = debone<std::string_view>("hello world"s);
// compiles quietly, and has UB: x.begin() dangles
auto x = debone<std::span<const int>>({{1,2,3}});
// compiles quietly, and has UB, but will likely work "in practice" (*)
auto x = debone<std::span<const int>>(std::vector{1,2,3}});
// compiles quietly, and has UB: x.begin() dangles
(* — 请参阅P2752 "静态存储用于花括号初始化器", 该提案在2023年被采纳,解释了为什么这在实践中可能有效。但它仍然在其实际生命周期之外访问了后备数组。)
我假设span
和string_view
之所以“正确地”将自己标榜为借用视图,是因为它们期望有时被用作值类型而不仅仅是“仅限参数类型”(这也是它们提供operator=
和swap
的原因)。但是initializer_list
几乎是一个“纯粹”的仅限参数类型 - 甚至有人认真提议消除其operator=
。initializer_list
并不期望被有意地传递给Ranges算法。因此,它不费心将自己标榜为借用范围或视图。
initializer_list
改为std::array
,就可以运行了 https://godbolt.org/z/rPqs5xhxT,但不幸的是,无法用于 range-v3。 - Tom Huntington