🧊

C++23 <ranges>のviewを見る3 - As const view

2023/04/27に公開

views::as_const

As const viewは、入力シーケンスの各要素をstd::as_constしたような定数要素からなるシーケンスを生成するviewです。

As const viewを生成するには、views::as_constを使用します。

import std;

int main() {
  std::vector vec = {1, 2, 3, 4};
  
  for (auto& n : vec | std::views::as_const) {
    n = 0; // ng!
  }
}

間違えるとコンパイルエラーになるのでまず間違わないとは思いますが、std::as_conststd::views::as_constは別物なので注意が必要です。

views::as_constした範囲は各要素がconst化されており、以降その範囲の操作を通して要素を変更することができなくなります。

このviewは範囲forによるイテレーション時よりは、自分の所有する範囲をライブラリ関数に安全に渡したい場合により有効でしょう。

import std;

// 渡した範囲が内部でどう使われるかわからない
auto iterate_range(std::ranges::range auto&& r) {
  ...
}

int main() {
  // 知らないところで変更されたくない範囲
  std::vector input = {1, 2, 3, 4};
  
  iterate_range(input);  // もしかしたら要素を勝手に変更されるかもしれない
  iterate_range(input | std::views::as_const);  // 変更の心配がない
  
  // vecをconstにしたくない
  input.push_back(5);
}

あるいは、この場合のinputconst化してもその要素までconstにならない場合があり(std::spanなど)、views::as_constはその場合でもその要素をconst化できます。

Rangeアダプタオブジェクト

views::as_constはRangeアダプタオブジェクトであり、入力範囲r(型をR、全修飾を取り除いた型をUとする)によってviews::as_const(r)のように呼ばれた時

  • views::all_t<R>constant_rangeである時 : views::all(r)
    • Rが既にconstant_rangeである時、viewに変換するだけ
  • Uempty_view<X>である時 : auto(views::empty<const X>)
    • auto(expr)exprdecay-copyすること
  • Uspan<X, E>である時 : span<const X, E>(r)
  • Uref_view<X>であり、const Xconstant_rangeのモデルとなる時 : ref_view(static_cast<const X&>(r.base()))
  • rが左辺値であり、const Uconstant_rangeのモデルとなる時 : ref_view(static_cast<const U&>(r))
  • それ以外の場合 : as_const_view(r)

いずれの場合もrはその値カテゴリによって適切にムーブされます。

少し複雑ですが、ここで行われている分岐は、単に型を少し工夫するだけでその要素のconst化が達成できる場合にそうするようにしているだけです。要素のconst化のために追加の作業が必要となる場合(一番最後のケース)は、as_const_viewに委譲されます。

constant_rangeはC++23で追加された新しいコンセプトで、各要素がconstになっているようなrangeを定義するものです。

// <ranges>内
namespace std::ranges {
  // constant_rangeの宣言例
  template<class T>
  concept constant_range = 
    input_range<T> && 
    constant-iterator<iterator_t<T>>;  // イテレータの参照型が`const`であるイテレータを表すコンセプト
}

このコンセプトはまた、任意の範囲を受け取る関数のインターフェースで、その要素を変更しないことを表明するためにも使用できます。この場合は逆に、変更可能な(constant_rangeではない)範囲を渡すとコンパイルエラーになります。

import std;

// 範囲の要素は変更されないことを表明
auto iterate_range(std::ranges::constant_range auto&& r) {
  ...
}

int main() {
  std::vector input = {1, 2, 3, 4};
  
  iterate_range(input);  // ng、これは変更可能な渡し方
  iterate_range(input | std::views::as_const);  // ok、変更不可能
}

コンセプトで制約しただけではその関数内で入力範囲を変更しないことを完全には保証できないため、constant_rangeは実際に変更不可能になっている範囲だけを受け入れます。

as_const_view

as_const_viewviews::as_constが入力範囲要素をconst化するための作業を行う際の実装詳細です。とはいえ、views::as_rvalueに対するas_rvalue_viewの時とほぼ同じように、その実態はstd::const_iteratorに委譲されています。

// as_const_viewの宣言例
namespace std::ranges {

  template<view V>
    requires input_range<V>
  class as_const_view : public view_interface<as_const_view<V>> {
    ...
    
    // 非const begin()
    constexpr auto begin() requires (!simple-view<V>)
    { return ranges::cbegin(base_); }

    // 非const end()
    constexpr auto end() requires (!simple-view<V>)
    { return ranges::cend(base_); }

    ...
  };
}

ranges::cbegin()/cend()はC++23で確実に要素変更不可能なイテレータを返すように改修され、ここでは入力Vbegin()/end()から取得されたイテレータをstd::const_iteratorに通して返します。

std::const_iterator

std::const_iterator<I>はエイリアステンプレートであり、Iの参照型がまだconstではない場合にstd::basic_const_iterator<I>を返すもので、views::as_constで使われる場合は常にstd::basic_const_iterator<I>となります。

std::basic_const_iterator<I>はC++23で追加されたイテレータラッパであり、イテレータIの間接参照結果をconst化するものです。

import std;

int main() {
  std::vector input = {1, 2, 3, 4};
  auto it = input.begin();
  
  *it = 0;  // ok
  
  std::basic_const_iterator<decltype(it)> cit = it;
  
  *cit = 0; // ng
}

as_const_view<V>はそのイテレータとしてVのイテレータをラップしたstd::basic_const_iteratorを常に用いることによって入力範囲の要素をconst化し、views::as_constは必要な場合にas_const_viewを返すことで全ての場合において入力範囲をその要素がconst化されたviewに変換します。

views::as_constの各種特性

入力範囲Vの参照型(range_reference_t<V>)をTとすると、views::as_constの全ての場合において次のようになります

  • reference
    • Tconstではない参照型の場合 : const T
    • Tconst参照型の場合 : T
    • それ以外(Tprvalue)の場合 : std::remove_cv_t<T>
  • rangeカテゴリ : Vのカテゴリと同じ
  • common_range : Vcommon_rangeである時
  • sized_range : Vsized_rangeである時
  • const-iterable : Vconst-iterableである時
  • borrowed_range
    • views::as_constas_const_view<V>を返す場合 : enable_borrowed_range<V>trueである時
    • それ以外の場合 : Vborrowed_rangeである時

ほとんど、入力範囲の性質をそのまま継承します。

referenceに関しては、入力範囲の参照型がstd::tupleの場合などは少し異なった結果(tuple要素をconstにする)になりますが、結局要素のconst化は達成されるようになっています。

入力範囲Vの要素がprvalueである場合、const化はされずそのまま素通しされてしまいます。少し注意点かもしれません。一応これによるメリットとして、views::as_constを通したとしても内部イテレータの間接参照結果からのコピー省略を妨げないようになっています。

import std;

int main() {
  std::vector vec = {1, 2, 3, 4};

  for (auto&& n : vec | std::views::transform([](int& m) { return m * 2;})
                      | std::views::as_const)
  {
    n = 0;  // ok!?  
  }
}

ただこの場合でも、views::as_const後の範囲の操作から、その入力範囲の各要素が変更できないという性質は成り立っています(間接参照結果のprvalueをどうしても元の範囲の要素に影響を与えないため)。

Discussion