[C++] <ranges>のviewを見る1 - empty_view

4 min読了の目安(約3600字TECH技術記事

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_viewviewコンセプトを満たしません。enable_viewviewコンセプトを有効化するための最後の一押しです。

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 factoriesrange 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_viewrangecontiguous range(イテレータがcontiguous iteratorの範囲)です。

range factories

std::viewsstd::ranges::views)名前空間にある関数オブジェクトを用いると空のViewを取得するという操作を若干簡潔に書くことができます。

#include <ranges>

int main() {
  for (int n : std::views::empty<int>) {
    assert(false);  // 呼ばれない
  }
}

std::views名前空間にあるこの様なViewクラスに対応する操作を表す関数オブジェクトの事を、range factoryと呼びます。