[C++] <ranges>のviewを見る5 - ref_view
range adaptors
range adaptorsは他のViewを含む任意のrangeに対して作用して、特定の操作を適用したViewに変換するものです。range adaptorはrangeからrangeへ操作を適用しつつ変換するものなので、range adaptorの結果にさらにrange adaptorを適用する形で操作をチェーンさせることができます。そして、その最終的な結果もまたrangeとして得られます。
range factoriesはシーケンスを生成するタイプのViewなのでrange adaptorsのように他のrangeに作用することはできませんが、range adaptorsと比較してみるとrange factoriesはrange adaptorsによるチェーンの起点となるViewであることが分かるでしょう。
range adaptor objects
range factoriesのView型にはstd::views
名前空間にその構築を簡略化するための関数オブジェクトなどが用意されていました。これと同様に、range adaptorsにもその構築を簡略化し明瞭にするための関数オブジェクトが用意されます。これらのものはrange adaptor objectsと呼ばれます。
range adaptor objectsは第一引数にrangeを取り、戻り値として対応するViewを返すカスタマイゼーションポイントオブジェクトとして定義されています。
|
)と関数呼び出し
パイプライン演算子(range adaptorは関数呼び出しによって使用するほかに、パイプライン演算子(|
)によっても使用することができます。パイプライン演算子によるスタイルはネストした関数呼び出しをその適用順に分解した形で書くことができ、コードの可読性の向上が期待できます。
同じrange adaptorについては、パイプラインスタイルと関数呼び出しスタイルはどちらを用いても同じViewが得られることが保証されています。
// R, R1, R2を任意のrange adaptorとする
// この2つの呼び出しは同じViewを返す
R(std::views::iota(1));
std::views::iota(1) | R ;
// この3つの呼び出しも同じViewを返す、さらにrange adaptorが増えても同様
R2(R1(std::views::iota(1)));
std::views::iota(1) | R1 | R2;
std::views::iota(1) | (R1 | R2);
// range adopterが追加の引数を取るときでも、次の3つは同じViewを生成する
R(std::views::iota(1), args...);
R(args...)(std::views::iota(1));
std::views::iota(1) | R(args...)
なお、このパイプライン演算子は新しい演算子ではなくて既存のビット論理和演算子をオーバーロードしたものです。
ref_view
ref_view
は他のrangeを参照するだけのViewです。
#include <ranges>
int main() {
std::vector vec = {1, 3, 5, 7, 9, 11};
for (int n : std::ranges::ref_view(vec)) {
std::cout << n; // 1357911
}
}
右辺値でさえなければ任意のrangeを受けることができて、そのrangeを単に参照するだけのViewとなります。ref_view
のrangeのカテゴリは参照しているrangeのものを受け継ぎます。
ぱっと見ると何に使うのか不明ですが、これはstd::vector
などのコピーが気軽にできないrangeの取り回しを改善するために利用できます。そのようなrangeの軽量な参照ラッパとなることで、コピーされるかを気にしなくてよくなるなど可搬性が向上します。役割としては、通常のオブジェクトの参照に対するstd::reference_wrapper
に対応しています。
例えば、std::async
の追加の引数として渡すときなどのようにdecay copyされてしまう所に渡す場合に活用できるでしょう。
struct check {
check() = default;
check(const check&) {
std::cout << "コピーされたよ!" << std::endl;
}
};
int main()
{
std::vector<check> vec{};
vec.emplace_back();
[[maybe_unused]]
auto f1 = std::async(std::launch::async, [](auto&&) {}, vec); // vectorがコピーされる
[[maybe_unused]]
auto f2 = std::async(std::launch::async, [](auto&&) {}, std::ranges::ref_view{vec}); // ref_viewがコピーされる
}
また、C++20のRangeライブラリの元となったRange-V3ライブラリではzip_view
を構成するためにも利用されているようです。
例えば、Viewを作ろうとするとデフォルト構築とムーブ構築/代入が少なくとも求められますが、rangeへの参照を持つと代入演算子やデフォルトコンストラクタが定義ができなくなり、ポインタを利用する場合はnullptr
を気にしなければなりません。view
コンセプトによるViewの定義を思い出すと、すべてのViewはデフォルト構築可能でムーブ構築/代入が可能であり、ref_view
もまたそれに従います。
このように自分でViewを作成する時など、他のrangeをクラスメンバに持って参照したいときに直接そのrangeの参照を持つ代わりに利用することもできます。
比較的短いので定義も見てみましょう。
namespace std::ranges {
template<range R>
requires is_object_v<R>
class ref_view : public view_interface<ref_view<R>> {
private:
R* r_ = nullptr; // 説明専用メンバ変数
public:
constexpr ref_view() noexcept = default;
template<not-same-as<ref_view> T>
requires /*see below*/
constexpr ref_view(T&& t);
constexpr R& base() const { return *r_; }
constexpr iterator_t<R> begin() const { return ranges::begin(*r_); }
constexpr sentinel_t<R> end() const { return ranges::end(*r_); }
constexpr bool empty() const
requires requires { ranges::empty(*r_); }
{ return ranges::empty(*r_); }
constexpr auto size() const requires sized_range<R>
{ return ranges::size(*r_); }
constexpr auto data() const requires contiguous_range<R>
{ return ranges::data(*r_); }
};
template<class R>
ref_view(R&) -> ref_view<R>;
}
このように、ref_view
自体は対象のrangeへのポインタを保持し、参照するrangeのイテレータをそのまま利用します。
views::all
ref_view
に対応するrange adaptor objectがstd::views::all
です。
#include <ranges>
int main() {
std::vector vec = {1, 3, 5, 7, 9, 11};
for (int n : std::views::all(vec)) {
std::cout << n;
}
std::cout << '\n';
// パイプラインスタイル
for (int n : vec | std::views::all) {
std::cout << n;
}
}
views::all
はカスタマイゼーションポイントオブジェクトであり、1つの引数(r
)を受け取りそれに応じて次の3つのいずれかの結果を返します。
-
r
がViewである(std::decay_t<decltype(r)>
がstd::ranges::view
コンセプトを満たす)ならば、r
をdecay copyして返す -
ref_view{r}
が構築可能ならば、ref_view{r}
それ以外の場合、std::ranges::subrange{r}
- それ以外の場合、
std::ranges::owning_view{r}
厳密にはref_view
だけを生成するわけではないのですが、結果の型を区別しなければ実質的にref_view
相当のViewを得ることができます。
Discussion