[C++]std::common_referenceの概念

C++20より追加されたstd::common_reference<T, U>は型T, U両方から変換可能な共通の参照型を求めるメタ関数です。ただしその結果型は必ずしも参照型ではなかったりします。std::common_typeとの差など、存在理由がよく分からない物でもあります・・・

同じ事を思う人は他にも居たようでstackoverflowで質問している人がいて、そこにcommon_referenceを考案したEric Nieblerさん御本人が解説を書いていました。本記事はそれを日本語で説明し直したものです。

イテレータ::reference::value_typeの関連性

標準ライブラリのイテレータ::reference::value_typeという二つの入れ子型を持っています。referenceは通常*itの戻り値型であり、value_typeはイテレートしているシーケンスの要素型(constや参照型ではない)を表します。そして、referencevalue_type&もしくはそのconst付きの型です。これによって次のような操作が可能です。

//要素型を明示的にコピーしたい場合など?
value_type t = *it;

このように、これら2つの型の間には通常自明な関係性が存在しています。そして、それら2つの型はconst value_type&という共通の型に変換可能であるはずで、実際ほとんどのイテレータはそうなっています。

より高機能なイテレータ

rangeライブラリのようなシーケンスを抽象化して扱うライブラリ(例えばC#LINQなど)によくある操作に、2つのシーケンスのzipという操作があります。zipは2つのシーケンスをまとめて1つのシーケンスとして扱う操作です(2つのシーケンスを1つのシーケンスに圧縮するからzip?)。2つのシーケンスの同じ位置の要素をペアにして、そのペアのシーケンスを生成する操作とも言えます。その場合、わざわざ元のシーケンスをコピーしてから新しいシーケンスを作成なんてことをするはずもなく、イテレータを介してそのようなシーケンスを仮想的に作成します。

その時、そのイテレータzip_iteratorと呼ぶことにします)の::reference::value_typeはどうなっているでしょう?例えば、std::vector<int>std::vector<double>をzipしたとすれば、その場合のzip_iteratorの2つの入れ子型は次のようになるでしょう。

  • reference : std::pair<int&, double&>
  • value_type : std::pair<int, double>

見て分かるように、これらの型はもはやconst value_type&という型で受けることはできず、value_type t = *itのような操作もできません。2つの型の間の関連性は失われてしまっています。

しかし、この場合でもこの2つの型の間に何の関連も無いという事はなく、なにかしら関連性があるように思えます。しかし、どのような関連性を仮定し利用できるのでしょうか・・・?

std::common_reference

std::common_referenceはそれに対する1つの解(あるいは要求)です。つまり、イテレータ種別に関わらずそのreferencevalue_typeの間には共通の参照型(従来のイテレータにおけるconst value_type&)に相当するものがあるはずであり、あるものとして、ジェネリックな操作において(すなわち任意のイテレータについて)その仮定を安全なものであると定めます。

そして、そのような共通の参照型に相当するものを統一的に求め、表現するために用意されたのがstd::common_referenceメタ関数です。

これは次のような操作が常に可能であることを保証します。

template<typename Iterator>
void algo(Iterator it, Iterator::value_type val) {
  using CR = std::common_reference<Iterator::reference, Iterator::value_type>::type;

  //共に束縛可能
  CR r1 = *it;
  CR r2 = val;
}

このように、common_referenceは従来のイテレータ型のreferencevalue_typeの間の関連性をより一般化したものであり、ジェネリックな処理においてzip_iteratorのようなより一般的なイテレータを区別なく扱うために必要なものであることが分かります。

なお、名前にreferencceとあるのは従来のイテレータにおけるcommon_referenceconst value_type&という参照型であったことから来ていると思われ、より一般的なcommon_referenceは必ずしも参照型ではなく、そうである必要もありません。

std::common_reference_withコンセプト

ある型のペアがcommon_referenceを有しているかをstd::common_reference<T, U>::typeが有効かを調べて判定するなどということは前時代的です。C++20にはコンセプトがあり、それで判定すればいいはず。

ということで?C++20ではそのような用途のためにstd::common_reference_withコンセプトが<concepts>に用意されています。これは、std::common_reference_with<T, U>のように2つの型の間にcommon_referenceがあることを表明(要求)するそのままのものです。

C++20にzip_viewが無いのは・・・

std::common_referenceの動機付けでもあったzip_iteratorを返すzip_viewのようなrange操作はC++20には導入されていません(同じ事情を持つものにはstd::vector<bool>が既にあります)。なぜかといえば、zip_iteratorについてのcommon_referenceを単純には求められなかったためです。再掲になりますが、zip_iteratorreferencevalue_typeは一般的には次のようになります。

  • reference : std::pair<T&, U&>
  • value_type : std::pair<T, U>

この場合のcommon_referenceは単純にはstd::pair<T&, U&>になるでしょうが、現在のstd::pairstd::pair<T, U> -> std::pair<T&, U&>のような変換はできません。C++20でもそれは可能になってはいません。かといって、zip_viewのためだけにpair-likeな型を用意するのも当然好まれません。

この問題をどうするのかの議論はされていますが、結論がC++20には間に合わなかったためC++20にはzip_viewはありません。

そして、この影響を受けてしまったのがstd::flat_mapです。C++20入りを目指していましたが、パフォーマンスのためにそのKeyのシーケンスとvalueのシーケンスをそれぞれ別で持つという設計を選択していたため、要素のイテレートのためにzip_viewが必要となりました。しかし、zip_viewは延期されたのとその設計からくる問題があったのとでstd::flat_mapも延期され、それに引きずられてstd::flat_setC++20には入りませんでした。

どれも将来的には入るとは思いますが、C++20で使いたかった・・・

コンセプト定義に現れるcommon_reference_with

std::common_referenceイテレータ型を受け取るのではなく2つの型を受け取ってその間のcommon_referenceを求めます。common_referenceの動機付けはイテレータ型からのものでしたが、そこから脱却すれば、common_referenceはより一般化した2つの型の間の関連性ととらえることができます。

そのようなcommon_referenceとは、2つの型に共通している部分を表す型だと見ることができます。std::common_type集合論的な意味での共通部分に相当していますが、std::common_referenceはそのような共通部分を参照、あるいは束縛できる型を表します。つまり、std::common_referencestd::common_typeを包含しています。
このことは逆に言えば、common_referenceを持つ2つの型は何かしら共通した部分を持つ、という事です。それはおそらく基底クラスもしくは同じ型のメンバであるでしょう。

そして、この性質は標準ライブラリにおけるコンセプト定義において利用されます。例えば、std::totally_ordered_with<T, U>std::swappable_with<T, U>のように2つの型の間で可能な性質を表明するコンセプトの定義においてほぼ必ずstd::common_reference_with<T, U>が現れています(そのようなコンセプトは多くが~_withという命名になっています)。
これらのようなコンセプトが表明する関係は明らかに2つの型の間に共通した部分があることを前提にしています。std::common_reference_withを定義中に使用しているのは、そのような関連性があることを表明し、要請するためです。

例えば、std::totally_ordered_with, std::equality_comparable_with, std::three_way_comparable_withなど比較に関するコンセプトなら、比較可能であるという事は2つの型の間に比較可能な部分がある筈ですし、std::swappable_withならば2つの型の間に交換可能な共通の部分がある筈です。

std::common_referencestd::common_reference_withは一見すると意味不明で何に使うのか良く分かりませんが、ここまでくるとようやく意味合いが見えてくるでしょうか。特に、コンセプト定義におけるstd::common_reference_with意味のあるコンセプトを定義するのに役立つかもしれません。

参考文献

この記事のMarkdownソース