C++23 <ranges>のviewを見る3 - As const view
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_const
とstd::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);
}
あるいは、この場合のinput
をconst
化してもその要素まで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
に変換するだけ
-
-
U
がempty_view<X>
である時 :auto(views::empty<const X>)
-
auto(expr)
はexpr
をdecay-copyすること
-
-
U
がspan<X, E>
である時 :span<const X, E>(r)
-
U
がref_view<X>
であり、const X
がconstant_range
のモデルとなる時 :ref_view(static_cast<const X&>(r.base()))
-
r
が左辺値であり、const U
がconstant_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_view
はviews::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で確実に要素変更不可能なイテレータを返すように改修され、ここでは入力V
のbegin()/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
-
T
がconst
ではない参照型の場合 :const T
-
T
がconst
参照型の場合 :T
- それ以外(
T
がprvalue)の場合 :std::remove_cv_t<T>
-
-
range
カテゴリ :V
のカテゴリと同じ -
common_range
:V
がcommon_range
である時 -
sized_range
:V
がsized_range
である時 -
const-iterable
:V
がconst-iterable
である時 -
borrowed_range
-
views::as_const
がas_const_view<V>
を返す場合 :enable_borrowed_range<V>
がtrue
である時 - それ以外の場合 :
V
がborrowed_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