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
や参照型ではない)を表します。そして、reference
はvalue_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つの解(あるいは要求)です。つまり、イテレータ種別に関わらずそのreference
とvalue_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
は従来のイテレータ型のreference
とvalue_type
の間の関連性をより一般化したものであり、ジェネリックな処理においてzip_iterator
のようなより一般的なイテレータを区別なく扱うために必要なものであることが分かります。
なお、名前にreferencce
とあるのは従来のイテレータにおけるcommon_reference
がconst 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_iterator
のreference
とvalue_type
は一般的には次のようになります。
reference
:std::pair<T&, U&>
value_type
:std::pair<T, U>
この場合のcommon_reference
は単純にはstd::pair<T&, U&>
になるでしょうが、現在のstd::pair
はstd::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_set
もC++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_reference
はstd::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_reference
とstd::common_reference_with
は一見すると意味不明で何に使うのか良く分かりませんが、ここまでくるとようやく意味合いが見えてくるでしょうか。特に、コンセプト定義におけるstd::common_reference_with
は意味のあるコンセプトを定義するのに役立つかもしれません。
参考文献
- What is the purpose of C++20 std::common_reference? - stackoverflow
- P0022R1 Proxy Iterators for the Ranges Extensions
- std::common_reference - cpprefjp
- P1727R0 Issues with current flat_map proposal
- Trip Report: ISO C++ Meeting Cologne (2019) by Matthias Gehre
std::flat_map
とzip_view
についての議論に触れられていたが、リンク切れしている・・・