[C++] <ranges>のviewを見る1 - empty_view
Viewについて
rangeライブラリにおけるViewとは、他の所(言語、ライブラリ、概念・・・)での任意のシーケンスに対するViewと呼ばれるものと同じ意味です。元のシーケンスに対して何か操作を適用した結果得られ、元のシーケンスをコピーせず参照し、かつ遅延評価によってシーケンスに操作を適用するものです。
さらに、View自身もシーケンスなのでViewに対してさらに他の処理を適用していくことができるようになっています。
rangeライブラリにおけるViewはコンセプトによって構文・意味論の両方向から次のように定義されます。
template<class T>
concept view =
range<T> && // begin()/end()によってイテレータペアを取得可能
movable<T> && // ムーブ可能
default_initializable<T> && // デフォルト構築可能
enable_view<T>; // viewコンセプトを有効化する変数テンプレート
- ムーブ構築/代入は定数時間
- デストラクトは定数時間
- コピー不可もしくは、コピー構築/代入は定数時間
この定義に沿う型がrangeライブラリにおけるViewとして扱われます。
分かりづらいかもしれませんが意味するところはすなわち、任意のシーケンスを所有せずに参照し、View自身の構築・コピー・ムーブ・破棄は参照する範囲とは無関係であるということです。
実際の実装はほぼ間違いなくイテレータペア(range)を保持するクラス型となり、Viewにまつわる操作はbegin()
の呼び出し時、あるいはそのイテレータに対する++, *
等の操作のタイミングで実行されることによって遅延評価されることになるでしょう。
標準ライブラリにあるViewであるクラス型にはたとえばstd::string_view
があります。std::string_view
は自身もrangeであり、ムーブやデフォルト構築が可能で、単に文字列の先頭ポインタと長さを保持するものなので、構文的にも意味論的にもこの定義に沿っています。
ただし、std::ranges::enable_view<std::string_view>
(上記コンセプト定義の一番最後の条件)がfalse
となるのでstd::string_view
はview
コンセプトを満たしません。enable_view
はview
コンセプトを有効化するための最後の一押しです。
view_interface
<ranges>
にあるViewとなるクラスは共通部分の実装を簡略化するためにview_interface
というクラスを継承しています。view_interface
はCRTPによって派生クラス型を受け取り、派生しているViewに対してコンテナインターフェースを備えるためのものです。
これによって、empty()/data()/size()/front()/back()/operator[]
と言った要素を参照する操作が利用可能となります(ただし、Viewの参照するrangeの種類(すなわち、イテレータカテゴリ)によります)。
template<class D>
requires is_class_v<D> && same_as<D, remove_cv_t<D>>
class view_interface : public view_base {
// 略
};
view_base
というのは単なるタグ型で、Viewとなるクラスを識別するためのものです。Viewの型D
に求めらているのはクラス型でありCV修飾されていない事だけです。自分でViewを定義する時もこれを利用すると良いでしょう。ちなみに、これを継承しておくとstd::ranges::enable_view<T>
が自動的にtrue
となるようになっています。
Viewの命名規則と操作
<ranges>
にあるViewは操作名_view
という名前でクラスとしてstd::ranges
名前空間に定義されており、rangeオブジェクトを渡して構築することでその操作を行うViewオブジェクトを得ることができます。そして、その操作に対応するview
を作成するための関数オブジェクトがstd::ranges::views
名前空間に操作名_view
に対して操作名
で定義されています。こちらを用いるとViewを得る操作を簡潔に書くことができます。
名前空間名は真面目に書くと長いですがstd::views
という名前空間エイリアスが用意されており、そちらを用いると少し短く書けます。
この様な関数オブジェクトには、range factoriesとrange adaptor objectsの2種類があります。
empty_view
empty_view<T>
は型T
の空のシーケンスを表すViewです。
#include <ranges>
int main() {
std::ranges::empty_view<int> ev{};
for (int n : ev) {
assert(false); // 呼ばれない
}
}
これは次のように定義されます。
namespace std::ranges {
template<class T>
requires is_object_v<T>
class empty_view : public view_interface<empty_view<T>> {
public:
static constexpr T* begin() noexcept { return nullptr; }
static constexpr T* end() noexcept { return nullptr; }
static constexpr T* data() noexcept { return nullptr; }
static constexpr size_t size() noexcept { return 0; }
static constexpr bool empty() noexcept { return true; }
};
namespace views {
// 変数テンプレート
template<class T>
inline constexpr empty_view<T> empty{};
}
}
使いどころはすぐには思いつきませんが、rangeを取るアルゴリズムに対してあえて空のrangeを渡したい場合に利用することができるでしょうか。そのような場合、型T
を与えるだけで空のrangeを得ることができるのでお手軽です。
この定義からわかるように、empty_view
のrangeはcontiguous range(イテレータがcontiguous iteratorの範囲)です。
range factories
std::views
(std::ranges::views
)名前空間にある関数オブジェクトを用いると空のViewを取得するという操作を若干簡潔に書くことができます。
#include <ranges>
int main() {
for (int n : std::views::empty<int>) {
assert(false); // 呼ばれない
}
}
std::views
名前空間にあるこの様なViewクラスに対応する操作を表す関数オブジェクトの事を、range factoryと呼びます。
Discussion