[C++]C++20からのiterator_traits事情

これはC++ Advent Calendar 2020の14日めの記事です。

C++20のiterator_traitsには、C++17以前のコードに対する互換レイヤとしての複雑な役割が与えられるようになります。従って、C++20からのイテレータ情報の問い合わせには前回説明したものを利用するようにする必要があります。

結論だけ先に書いておくと

前回も合わせてお読みいただくと理解が深まるかもしれません。

目次

以下、特に断りが無ければIイテレータ型だと思ってください。

C++20におけるiterator_traitsの役割

前回見たように、C++20以降はイテレータ利用にあたってはiterator_traitsを利用する必要は全く無くなっています。それに伴ってiterator_traitsにはC++20イテレータC++17互換イテレータとして利用するための互換レイヤとしての役割が新たに与えられています。

どういう事かというと、C++20のイテレータC++17イテレータから求められることが変化しており、C++20イテレータにはC++17イテレータに対する後方互換性がありません。そのために、C++17のコードからC++20以降のイテレータを利用しようとすると謎のコンパイルエラーが多発する事になるでしょう。
そんな時でも、iterator_traitsC++17以前のコードからは利用されているはずで、イテレータを利用する際は何かしらそれを介しているはずです。そこで、iterator_traitsイテレータ互換性のチェックとC++17イテレータへの変換を行う事にしたようです。

iterator_traitsによるイテレータカテゴリの取得経路

C++20のiterator_traits<I>は大まかには次のようにIiterator_categoryを取得しようとします。

  1. Iのメンバ型
    • iterator_categoryI::iterator_categoryから取得
  2. IC++17入力イテレータに準するのであれば、可能な操作から
    • iterator_categoryIが4つのC++17イテレータコンセプトのどれに準ずるかによって決定
  3. IC++17出力イテレータに準ずるのであれば、そう扱う
    • iterator_categoryoutput_iterator_tag
  4. 上記いずれでも取得できなかった場合、iterator_traits<I>は空

この手順内でC++17入力イテレータとか言っているものはコンセプトです。ただし、C++20から使用可能となっている各種イテレータコンセプトではなく、C++17までのイテレータで要求されていたことを構文的に列挙しただけのとても簡易なものです。
これらのC++17イテレータコンセプトはC++20イテレータコンセプトほど厳密ではなく、同じカテゴリでも要件が異なるため互換性がありません。

結果、2番目の手順で取得されるiterator_categoryC++17基準の判定によって決定されます。

イテレータコンセプトによるイテレータカテゴリの取得経路

C++20で提供される、std::input_iteratorをはじめとする各イテレータカテゴリを定義するコンセプトの事をまとめて イテレータコンセプト と呼びます。

std::output_iteratorを除く5つのイテレータコンセプトは、ITER_CONCEPTという操作(説明専用のエイリアステンプレート)によってイテレータカテゴリを取得します。

そして、取得されたカテゴリタグ型がどのカテゴリタグ型から派生しているかを調べて、型Iがどのイテレータカテゴリを表明しているのかを取得します。たとえば、std::random_access_iteratorならば、std::derived_from<ITER_CONCEPT(I), std::random_access_iterator_tag>のようにチェックします。継承関係もチェックすることで、より強いイテレータも含めて判定でき、将来的なイテレータカテゴリの増加にも備えています。
もちろんこれだけではなく、Iが備えているインターフェースがそのカテゴリのイテレータとして適格であるかかもチェックされます。

ITER_CONCEPT(I)

ITER_CONCEPT(I)は次のようにIからイテレータカテゴリを取得します。

  1. iterator_traits<I>の明示的特殊化が無い場合(iterator_traitsのプライマリテンプレートが使用される場合)
    1. I::iterator_conceptから取得
    2. I::iterator_categoryから取得
    3. random_access_iterator_tagを取得
  2. iterator_traits<I>の明示的特殊化がある場合
    1. iterator_traits<I>::iterator_conceptから取得
    2. iterator_traits<I>::iterator_categoryから取得
    3. ITER_CONCEPT(I)は型名を示さない

1-3による手順はフォールバックです。イテレータカテゴリが取得できない場合はとりあえずランダムアクセスイテレータとして扱っておいて、イテレータコンセプトの他の部分で判定するようにするためのものです。

iterator_traitsによるカテゴリの取得時と大きく異なるところは、iterator_conceptがあればそれを優先する点にあります。

itereator_conceptiterator_category

iterator_conceptC++20からのイテレータカテゴリ表明のためのメンバ型です。やっていることはiterator_categoryと同じです。

これは主にポインタ型の互換性を取るために導入されたもので(C++20からポインタ型のイテレータカテゴリはcontiguous iteratorとなる)、それまで使用されていたiterator_categoryを変更しないようにカテゴリを更新しようとするものです。
それはiterator_traitsのポインタ型に対する特殊化に見ることができます。

namespace std {
  template<class T>
    requires is_object_v<T>
  struct iterator_traits<T*> {
    // C++20からのカテゴリ
    using iterator_concept  = contiguous_iterator_tag;
    // C++17までのカテゴリ
    using iterator_category = random_access_iterator_tag;
  
    using value_type        = remove_cv_t<T>;
    using difference_type   = ptrdiff_t;
    using pointer           = T*;
    using reference         = T&;
  };
}

これによって、Iiterator_categoryを見ればC++17までのイテレータとしてのカテゴリが、iterator_conceptを見ればC++20からのイテレータとしてのカテゴリがそれぞれ取得できることになります。

つまりは、C++20イテレータ型に対してそこからiterator_categoryを取得できる場合、そのC++20イテレータC++17イテレータとして扱うことができる事を意味しています。

また、iterator_conceptメンバ型を定義するということは、イテレータコンセプト、ひいてはC++20イテレータへの準拠を表明することでもあります。

iterator_traitsを通して見るC++20イテレータ

iterator_traitsを介してイテレータカテゴリを取得する時、そのイテレータiterator_categoryメンバ型を取得することになります。
iterator_categoryメンバ型はC++17以前のコードに対する互換性のために、C++17イテレータとしてのカテゴリを表明しています。

もしiterator_categoryメンバ型が無い場合、そのイテレータの可能な操作に基づくC++17の要件によって、適切なイテレータカテゴリが取得されます。

従って、iterator_traitsを通してC++20イテレータを見てみると、C++17互換イテレータとして見えることになります。

逆に、iterator_traitsを通してC++17イテレータを見た時はC++17までの振る舞いとほぼ変わりません。C++17イテレータはそのままC++17イテレータとして見えます。

標準のC++20イテレータ型(たとえば、<ranges>の各種viewイテレータ)には、そのメンバ型としてiterator_conceptiterator_categoryを両方同時に提供して、iterator_categoryの方を例えばinput_iterator_tagなど弱めることで安全にC++17イテレータとして利用できるようになっているものがあります。

イテレータコンセプトを通して見るC++17イテレータ

C++17のイテレータイテレータコンセプトに渡したときは、ITER_CONCEPTを通してイテレータカテゴリがI::iterator_cateogryから取得され、コンセプトによってそのインターフェースがチェックされます。C++17イテレータとして定義されていて、C++20での要件も満たしている場合は問題なくそのカテゴリのC++20イテレータとして判定されるでしょう。
多くの場合はC++17イテレータでは各カテゴリにおいての要件がC++20のものよりも厳しいはずなので、正しくC++17イテレータとして定義されていれば問題なくC++20イテレータとなれるはずです。

もちろん、C++17まではそれを判定する仕組みはなかったので思わぬところで要件を満たしていない可能性はあります。その時は、君がイテレータだと思ってるそれ、イテレータじゃないよ?ってコンパイラくんが教えてくれます。親切ですね・・・

逆に、イテレータコンセプトを通してC++20イテレータを見た時、ITER_CONCEPTを通してiterator_conceptが取得され、あるいはなかったとしても、最終的にコンセプトによって定義されたC++20イテレータとしての性質を満たしているかによって判定されます。
C++20のイテレータC++20のイテレータとして見ることができるのはこの経路だけです。

まとめると、それぞれからイテレータを見た時にどう見えるかは、次のようになります。

窓口 \ イテレータ C++17イテレータ C++20イテレータ
iterator_traits C++17イテレータ C++17イテレータ
イテレータコンセプト C++20イテレータ C++20イテレータ

こうしてみると古いものからは古いものとして、新しいものからは新しいものとして見える、というなんだか当たり前の話に見えてきます。

iterator_traitsの明示的特殊化

ここまであえて深く触れていませんでしたが、iterator_traitsを使おうとイテレータコンセプトを使おうと、必ず考慮しなればならない経路がもう一つあります。それはiterator_traitsIについて明示的に特殊化されていた場合です。典型的な例はポインタ型です。

その場合、iterator_traitsにせよITER_CONCEPTにせよ、特殊化iterator_traits<I>を最優先で使用するようになります。

iterator_traitsを直接使うのはC++17以前のコードがメインだと思われるので、そこにiterator_conceptメンバが生えていても触られることはないでしょう。

ITER_CONCEPT(I)では、I::iterator_conceptは無視されiterator_traits<I>::iterator_conceptがあればそれを、なければiterator_traits<I>::iterator_categoryを取得します。

どちらにせよ重要なことは、iterator_traitsIについて明示的に特殊化されている場合は、Iのメンバや実際の性質は無視して特殊化に定義されているものが取得されるという事です。

これは、元のイテレータIに対して非侵入的なカスタマイゼーションポイントになっています。最も重要なのはその特殊化にiterator_conceptメンバがあるかないかで、ある場合は元のイテレータが何であれC++20イテレータコンセプト準拠を表明することになり、ない場合は元のイテレータC++20イテレータであってもC++17イテレータとして扱われる、ということになります。

特殊化されたiterator_traits<I>::iterator_conceptメンバを触るのはITER_CONCEPT(I)だけですが、この場合であってもiterator_categoryの役割はC++17イテレータとして使用可能なカテゴリを表明する事なのが分かります。

iterator_traitsはプライマリテンプレートにせよ特殊化にせよC++17コードから利用されるものなので、iterator_categoryを含めた5つのメンバ型は必ず定義しておく必要があります。オプショナルなのはiterator_conceptのみです。

iterator_traitsの2つの役割

結局、C++20iterator_traitsには大きく次の二つの役割がある事が分かります。

  1. プライマリテンプレートが使用される場合、C++17互換レイヤとして機能する。
    この時、iterator_conceptメンバは定義されず、iterator_categoryのみがイテレータ型のC++17互換の性質によって定義される。
  2. 明示的に特殊化されている場合、イテレータのメンバ型よりも優先されるカスタマイゼーションポイントとして機能する。
    この時、iterator_conceptメンバが定義されないならば、iterator_categoryを使用してイテレータ型のC++20的性質を制限する。

C++20以降のイテレータ情報の問い合わせ、まとめ

(本当は前回の最後に載せるべきでしたが忘れていたのでここに置いておきます・・・)

情報 C++17 iterator_traits<I> C++20からの窓口
距離型 difference_type std::iter_difference_t<I>
要素の型 value_type std::iter_value_t<I>
要素の参照型 reference std::iter_reference_t<I>
要素のポインタ型 pointer なし
イテレータのカテゴリ iterator_category 各種イテレータコンセプト
要素の右辺値参照型 なし std::iter_rvalue_reference_t
イテレータcommon reference なし std::iter_common_reference_t

参考文献

この記事のMarkdownソース