范围和临时初始化列表

3

我试图将一个我认为是prvalue的值传递给一个范围适配器闭包对象。除非我绑定一个名称到初始化列表并使它成为lvalue,否则它不会编译通过。这里发生了什么?

#include <bits/stdc++.h>

using namespace std;

int main(){
  //why does this compile?
  auto init_list = {1,2,4};
  auto v = init_list | views::drop(1);
  
  //but not this?
  // auto v2 = initializer_list<int>{1,2,4} | views::drop(1);

  //or this?
  //auto v3 = views::all(initializer_list<int>{1,2,4}) | views::drop(1);
}

初始化列表用于初始化对象。您不应该将它们尝试视为创建要操作的值序列的简单且粗糙的方式。它们在这种情况下效果不佳。 - Nicol Bolas
initializer_list 改为 std::array,就可以运行了 https://godbolt.org/z/rPqs5xhxT,但不幸的是,无法用于 range-v3。 - Tom Huntington
2个回答

7
当您使用r | views::drop(1)创建一个新的view时,range适配器将自动将r转换为view。这要求r的类型必须模拟为viewable_range
template<class T>
  concept viewable_­range =
    range<T> &&
    ((view<remove_cvref_t<T>> && constructible_­from<remove_cvref_t<T>, T>) ||
     (!view<remove_cvref_t<T>> &&
      (is_lvalue_reference_v<T> || (movable<remove_reference_t<T>> && !is-initializer-list<T>))));

请注意最后一个要求:
(is_lvalue_reference_v<T> || (movable<remove_reference_t<T>> && !is-initializer-list<T>))));

由于r的类型并没有建模为view,在您的情况下是initializer_list,因此它需要是一个左值,允许我们自由地获取其地址以构造ref_view,而不必担心悬空。

或者,我们需要它是可移动的,这样我们就可以将其所有权转移给owning_view,这意味着以下结构是良好的:

auto r = std::vector{42} | views::drop(1)

但在这种情况下,我们也需要它不是initializer_list的特化版本,因为我们无法转移其所有权,initializer_list总是会复制。这就是你的示例失败的原因。


0

截至C++23的情况似乎是(Godbolt):initializer_list是一个连续的范围,但不是一个视图,也不是一个借用范围。这是令人费解的,因为实际上

  • initializer_list恰好是一个非拥有视图,它覆盖了其他地方存在的范围;复制一个initializer_list总是廉价且“浅而非深”。所以从理论上讲,你可能认为它是一个view

  • 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});
  // fails the static_assert -- good!

如果这段代码没有编译失败,那么返回的 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年被采纳,解释了为什么这在实践中可能有效。但它仍然在其实际生命周期之外访问了后备数组。)

我假设spanstring_view之所以“正确地”将自己标榜为借用视图,是因为它们期望有时被用作值类型而不仅仅是“仅限参数类型”(这也是它们提供operator=swap的原因)。但是initializer_list几乎是一个“纯粹”的仅限参数类型 - 甚至有人认真提议消除其operator=initializer_list并不期望被有意地传递给Ranges算法。因此,它不费心将自己标榜为借用范围或视图。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接