范围适配器的 begin/end 的 const 重载是否受限?

Is the const overload of begin/end of the range adapters underconstrained?

在 C++20 中,一些范围同时具有 const 和非 const begin()/end(),而其他范围仅具有非 const begin()/end() .

为了让包裹前者的范围适配器能够在const合格时使用begin()/end(),一些范围适配器如elements_view, reverse_view and common_view都提供了约束const-限定的begin()/end()函数,例如:

 template<view V>
   requires (!common_­range<V> && copyable<iterator_t<V>>)
 class common_view : public view_interface<common_view<V>> {
  public:
   constexpr auto begin();
   constexpr auto end();

   constexpr auto begin() const requires range<const V>;
   constexpr auto end()   const requires range<const V>;
};

template<input_­range V, size_t N>
  requires view<V> && has-tuple-element<range_value_t<V>, N>
class elements_view : public view_interface<elements_view<V, N>> {
 public:
  constexpr auto begin();
  constexpr auto end();

  constexpr auto begin() const requires range<const V>;
  constexpr auto end()   const requires range<const V>;
};

begin() const/end() const只有在const V满足range时才会被实例化,这似乎是合理的。

但是仅仅一个简单的约束似乎是不够的。以common_view为例,要求V不是common_range但是当V不是common_range并且const Vcommon_range时(我知道这样的范围非常奇怪,但是理论上是可以存在的,godbolt):

#include <ranges>

struct weird_range : std::ranges::view_base {
  int* begin();
  const int* end();

  std::common_iterator<int*, const int*> begin() const;
  std::common_iterator<int*, const int*> end()   const;
};

int main() {
  weird_range r;
  auto cr = r | std::views::common;
  static_assert(std::ranges::forward_range<decltype(cr)>); // ok
  cr.front(); // ill-formed
}

在上面的例子中,const V仍然满足range的概念,当我们将r应用到views::common时,它的front()功能将是格式错误。

原因是view_interface::front() const仍然会被实例化,而common_iterator会在common_viewbegin() const内部构造,这样会导致hard error中止编译,因为 const V 本身是 common_range.

同样,我们也可以根据相同的概念创建一个奇怪的范围,使views::reverseviews::keysfront()失败(godbolt):

#include <ranges>

struct my_range : std::ranges::view_base {
  std::pair<int, int>* begin();
  std::pair<int, int>* end();

  std::common_iterator<int*, const int*> begin() const;
  std::common_iterator<int*, const int*> end()   const;
};

int main() {
  my_range r;
  auto r1 = r | std::views::reverse;
  static_assert(std::ranges::random_access_range<decltype(r1)>); // ok
  r1.front(); // ill-formed

  auto r2 = r | std::views::keys;
  static_assert(std::ranges::random_access_range<decltype(r2)>); // ok 
  r2.front();  // ill-formed
}

那么,范围适配器 begin()/end()const 重载是欠约束的,还是 weird_range 本身的定义不正确?这可以被视为标准缺陷吗?


本题主要受到LWG 3592, which states that for lazy_split_view, we need to consider the case where the const Pattern is not range, and then I subsequently submitted the LWG 3599的启发。当我进一步查看其他范围适配器的 begin() const 时,我发现它们中的大多数只要求 const Vrange,这个看似松散的约束让我提出了这个问题。

为了启用范围适配器的 begin() const,理论上,const V 的约束应该与 V 完全相同,这意味着 long list of constraintsV 上,例如 elements_view,需要替换为 const V 而不是仅约束 const V 成为 range.

但实际上,似乎标准对Vconst V的iterator和sentinel类型相差很大的情况不感兴趣

最近 SG9 关于 LWG3564 的讨论得出的结论是,预期的设计是 xas_const(x) 应该被要求在保持相等的表达式中以相同的结果替换,而两者都是有效的。换句话说,它们在[concepts.equality]“相同的柏拉图价值观”意义上应该是“平等的”。因此,例如,xas_const(x) 具有完全不同的元素是无效的。

规则的确切措辞和范围必须等待论文,我们必须注意避免禁止合理的代码。但可以肯定的是,像“x 是一对的范围,但 as_const(x) 是一个整数的范围”之类的东西不在“合理”的任何合理定义范围内。