先日zennに投降した、views::as_const
についての記事を書いているときに調べた、const_iterator
の要素型(参照型)の決定過程に関するメモです。
以下、views::as_const
に関しては知っているものとして説明しません(この記事ないし他の解説をご覧ください)。
また、上記記事及び以降でも、イテレータの間接参照結果の事を指してイテレータの要素だとか要素型だとか言っていますが、それは正しくは要素の参照あるいは参照型(iter_reference_t
)の事です。本来の意味の要素型(iter_value_t
)とは異なり、views::as_const
やconst_iterator
は要素の間接参照結果をconst
化するのであって要素型そのものをconst
化するわけではありません。
- views::as_constとconstant_range
- std::ranges::cbegin()/cend()
- std::const_iterator/const_sentinel
- std::basic_const_iterator
- std::iter_const_reference_t
- 参考文献
views::as_const
とconstant_range
views::as_const
は入力範囲の要素がすでにconst
になっているかどうかを判定してからそうなっていない場合に何か作業を行います。その判定に関わっているのはviews::as_const
と共にC++23で追加されたstd::ranges::constant_range
コンセプトです。
// constant_rangeの定義 template<class T> concept constant_range = input_range<T> && constant-iterator<iterator_t<T>>;
constant_range
の主要な部分を担っているのがconstant-iterator
というコンセプトです。これは説明専用の(規格文書内で規定のために使用される)もので、次のように定義されています。
template<class It> concept constant-iterator = input_iterator<It> && same_as<iter_const_reference_t<It>, iter_reference_t<It>>;
constant-iterator
コンセプトの主要な部分は2つ目の制約式で、この制約式は入力のイテレータ型It
に対して、そのiter_const_reference_t
とiter_reference_t
が一致することを求めています。iter_reference_t
はイテレータの間接参照(*
)の直接の結果型のことで、どうやらそれがconst
になっていればこの制約式を満たせるように見えます。
このiter_const_reference_t
はviews::as_const
と共にC++23で追加されたエイリアステンプレートで、次のように定義されています
template<indirectly_readable It> using iter_const_reference_t = common_reference_t<const iter_value_t<It>&&, iter_reference_t<It>>;
イテレータ型It
に対して、その要素型(std::iter_value_t
)をconst
参照化したものと参照型のcommon_reference
を求めています。
common_reference
の決定過程は複雑ですが、雰囲気的にはこの結果は常にconst
が付いた型になりそうに見えます。とりあえず今はそれを認めることにして、iter_const_reference_t<I>
は常にconst
であることを仮定しておきます。
std::ranges::cbegin()/cend()
views::as_const
の入力範囲がconstant_range
ではない場合、views::as_const
は次にその型を何とかこねくり回して要素のconst
化が達成できないかを試行します。それが叶わない場合、入力範囲をas_const_view
に渡して返すことで要素型のconst
変換を行います。
views::as_const
がas_const_view
を使用する場合、そのイテレータはstd::ranges::cbegin()
から取得されます。ranges::cbegin()
はC++23で確実に要素がconst
になっているイテレータを返すように改修されており、型T
の式E
とそれの評価結果の左辺値をt
としてranges::cbegin(E)
のように呼ばれた時次のようなことを行います
enable_borrowed_range<remove_cv_t<T>> == false
ならば、ill-formed- そうではない場合、式
U
をranges::begin(possibly-const-range(t))
として、const_iterator<decltype(U)>(U)
を返す
possibly-const-range
は説明専用の関数であり、次のように定義されています
template<input_range R> constexpr auto& possibly-const-range(R& r) { if constexpr (constant_range<const R> && !constant_range<R>) { return const_cast<const R&>(r); } else { return r; } }
ここで行われていることは、入力範囲R
について入力範囲を単にconst
化すればその要素もconst
化する(const R
がconstant_range
となる)場合はそうして、そうでない場合、及びR
が既にconstant_range
である場合は入力をそのまま返す、という事をしています。
ranges::cbegin()
は、こうして返された範囲オブジェクトからranges::begin()
によってイテレータを取得し、それをstd::const_iterator
に通して返します。
ranges::cend()
はこの手順内のranges::begin()
をranges::end()
に置き換えたことを行い、最後にstd::const_sentinel
を通して返します。
std::const_iterator/const_sentinel
std::const_iterator
はエイリアステンプレートであり、入力の型I
によって次のどちらかの型を返します
I
がconstant-iterator
ならばI
- そうでないならば、
basic_const_iterator<I>
ここで出てくるconstant-iterator
コンセプトは、constant_range
で使用されていたものと同じものを指しています。すなわち、I
の要素型がconst
ではない場合にのみI
をstd::basic_const_iterator
にラップして返します。
std::const_sentinel
もエイリアステンプレートであり、入力の型S
によって次のどちらかの型を返します
S
がinput_iterator
ならばconst_iterator<S>
- そうでないならば、
S
これはどちらも、C++23でviews::as_const
と共に導入されたものです。
std::basic_const_iterator
std::basic_const_iterator
はC++23でviews::as_const
およびranges::cbegin()
のために追加されたイテレータラッパであり、入力イテレータの間接参照結果をconst
化する事だけを行います。
前述のように、イテレータ型I
に対していつもstd::const_iterator<I>
を使用するようにすれば、std::basic_const_iterator
をstd::basic_const_iterator
でラップするような二重const
化を回避してイテレータ要素のconst
化を達成できます。
std::basic_const_iterator
の見どころはほぼそのoperator*
のみで、それは内部のイテレータit
に対してreturn static_cast<reference>(*it)
を返します。reference
はstd::basic_const_iterator
の入れ子型として説明専用として定義されるもので、次のように定義されています
template<input_iterator Iterator> class basic_const_iterator { ... // 説明専用型referenceの定義 using reference = iter_const_reference_t<Iterator>; ... };
結局ここでも、iter_const_reference_t
が出てきます。
std::iter_const_reference_t
こうして、役所もびっくりのたらい回しの果てに、views::as_const
の要素型の決定にはstd::iter_const_reference_t
というエイリアステンプレートが深く関与していること分かりました。views::as_const
の分岐を考えると、これが直接介さない場合でもviews::as_const
の結果のview
の要素型(参照型)は入力範囲のイテレータ型をstd::iter_const_reference_t
に通して得られた型と同じになるはずです(たぶん)。
従って、views::as_const
の結果範囲の要素型はstd::iter_const_reference_t
がどう振舞うかを調べればわかることになります。std::iter_const_reference_t
の定義は次のようになっており
template<indirectly_readable It> using iter_const_reference_t = common_reference_t<const iter_value_t<It>&&, iter_reference_t<It>>;
その決定には2つの型のcommon_reference
が計算されています。
その1つ目の型const iter_value_t<It>&&
では、iter_value_t<It>
が参照型ではない場合に結果は必ずconst
になり、かつ常に参照型になります。とはいえiter_value_t<It>
は通常修飾なしの型(prvalue)であるはずで、std::indirectly_readable_traits
をよく見るとremove_cv
されるうえに参照型だとvalue_type
が定義されないことが分かるため、いつも修飾なしの型であるとします(もう一つiterator_traits
の経路だと必ずしもそうではありませんが)。すると、const iter_value_t<It>&&
とはいつもconst
右辺値参照になります。
2つ目の型iter_reference_t<It>
はイテレータの関節参照の結果型であり、これは
- 修飾なしの型(prvalue)
- 参照型
- 左辺値参照(
&
) - 右辺値参照(
&&
)
- 左辺値参照(
- 上2つの
const
修飾
のいずれかであるはずです(volatile
は無視で・・・)。
それらの型の間のcommon_reference
の決定は複雑ですがこれらの仮定を用いるとある程度求めやすくなります。特に、1つ目の型は常にconst
右辺値参照型なので、問題となるのは2つ目の型が参照型かどうかです。std::common_reference<const T&&, U>
とすると、この結果は次のどちらかになりそうです(あまり自信がない・・・)
U
が参照型(U&
/U&&
) :decltype(false ? declval<const T&(&)()>()() : declval<const U&(&)()>()())
U
がprvalue(U
) :decltype(false ? declval<const T&&(&)()>()() : declval<U(&)()>()())
とはいえこの型がどうなるかも難しいものがあり(そもそもパースがムズカシイ)、U
にconst
がついてると少し正しくない部分があるなどやはり難しいものがあります。なので、簡単なテスターを作って実際の振る舞いを確かめてみます。
template<typename T> using test = std::common_reference_t<const std::remove_cvref_t<T>&&, T>;
これを使うと、std::iter_const_reference_t
の型をイテレータ型を介さず直接調べることができます。
int main() { static_assert(std::same_as<test<int&>, const int&>); static_assert(std::same_as<test<int&&>, const int&&>); static_assert(std::same_as<test<const int&>, const int&>); static_assert(std::same_as<test<int>, int>); static_assert(std::same_as<test<const int>, int>); }
イテレータ型I
の参照型(iter_reference_t<I>
)をT
(+修飾)とし、型T
に対して特にbasic_common_reference
特殊化が行われていないとして、これによるとT
によってstd::iter_const_reference_t<I>
は次のようになります
iter_reference_t<I> |
iter_const_reference_t<I> |
---|---|
T& |
const T& |
T&& |
const T&& |
const T& |
const T&& |
const T&& |
const T&& |
T |
T |
const T |
T |
T
が参照型である場合は適切にconst
化されていますが、T
がprvalueである場合はそうなってはいません。それはまあ当然というか、prvalueをconst
参照でラップしたらそれはダングリング参照になりますし、prvalueにconst
を付加することにはほぼ意味がありません。
これは一般の型T
に対しての結果ですが、意図的にcommon_reference
が(basic_common_reference
によって)カスタムされている一部の型が標準ライブラリにも存在し、それを使用するようなrange
型ではその要素型と参照型の景色が少し異なっていたりします。たとえば
std::vector<bool>
range_value_t
:bool
range_reference_t
:std::vector<bool>::reference
views::zip
とそのファミリrange_value_t
:std::tuple<T, U>
range_reference_t
:std::tuple<T&, U&>
views::enumrate
range_value_t
:std::tuple<D, T>
range_reference_t
:std::tuple<D, T&>
などがあります。
ここでのT, U
はviews::zip
やviews::enumarate
の入力範囲のrange_value_t
/range_reference_t
/range_difference_t
によって変化するためそのプレースホルダーとします。T, U
は任意の型の可能性がありますがここでは参照修飾は考えないことにして、D
はprvalueの整数型です。
これらの型の場合は、値型(range_value_t
)・参照型(range_reference_t
)・const
要素型(range_const_reference_t
)はそれぞれ以下のようになります
range |
range_value_t<I> |
range_reference_t<I> |
range_const_reference_t<I> |
---|---|---|---|
std::vector<bool> |
bool |
std::vector<bool>::reference |
bool |
const std::vector<bool> |
bool |
bool |
bool |
views::zip |
std::tuple<T, U> |
std::tuple<T&, U&> |
std::tuple<const int&, const double&> |
views::enumrate |
std::tuple<D, T> |
std::tuple<D, T&> |
std::tuple<D, const T&> |
range_const_reference_t
はiter_const_reference_t
のrange
版で、iter_const_reference_t<iterator_t<R>>
として定義されています。
zip
とenumrate
の結果をみると想像がつくかもしれませんが、std::tuple<Ts...>
とstd::tuple<Us...>
のcommon_reference
とは、std::tuple<std::common_reference_t<Ts, Us>...>
のように、tuple
要素型の対応する型同士のcommon_reference
を求める形になります。なので、std::tuple
の他のケースはその要素型について先程の一般の型に対する結果を参照すると簡単に求められます。また、その場合はtuple
自身のconst
/参照修飾は無視されます。
iter_const_reference_t
はこのように、とても巧妙に入力イテレータ型の要素型をそれに応じて適切にconst
化します。しかも、prvalueの場合は余計なことをしないなど、本当によくできすぎていることが分かります。