チュートリアル:CRTP インターフェイス手法

ジェネリック コードは、型が特定の概念をモデル化することを想定しています。時には、その概念では、型に多くの冗長なメンバー関数が必要になることがあります。ここでの大きな原因は反復子です。反復子には多くの演算子のオーバーロードが必要であり、そのほとんどは他のオーバーロードに関して自明に実装されています。

不思議なことに繰り返されるテンプレート パターンである CRTP は、ここで役に立ち、ボイラープレートを自動化します。CRTP インターフェース手法を見て、その仕組みを調べてみましょう。

モチベーション

動機として、これを考慮してください 08 15 などのコンテナーの要素にアクセスします。 ポインタの代わりにインデックスを介して。そうすれば、 23 コンテナが再割り当てを行って要素を移動させても有効です。

template <typename Container>
class stable_iterator
{
    const Container* _container;
    std::size_t _index;

public:
    //=== Typedefs ===//
    using value_type     = typename Container::value_type;
    // for simplicity, no modification
    using reference_type = const value_type&;
    using pointer        = const value_type*;

    using difference_type   = std::ptrdiff_t;
    // for simplicity, no random access
    using iterator_category = std::forward_iterator_tag;

    //=== Constructors ===//
    // Create an invalid iterator.
    stable_iterator()
    : _container(nullptr), _index(0)
    {}

    stable_iterator(const Container& container, std::size_t idx)
    : _container(&container), _index(idx)
    {}

    //=== Access ===//
    reference_type operator*() const
    {
        assert(_container && _index < _container->size());
        return (*_container)[_index];
    }

    pointer operator->() const
    {
        // Address of reference returned by operator*().
        return &**this;
    }

    //=== Increment ===//
    stable_iterator& operator++()
    {
        assert(_container && _index < _container->size());
        ++_index;
        return *this;
    }

    stable_iterator operator++(int)
    {
        stable_iterator copy(*this);
        ++*this;
        return copy;
    }

    //=== Comparison ===//
    friend bool operator==(const stable_iterator& lhs,
                           const stable_iterator& rhs)
    {
        assert(lhs._container == rhs._container);
        return lhs._index == rhs._index;
    }

    // Not actually needed in C++20 due to operator rewrite rules.
    friend bool operator!=(const stable_iterator& lhs,
                           const stable_iterator& rhs)
    {
        return !(lhs == rhs);
    }
};

これは機能しますが、特に順方向反復子のみを実装したことを考えると、かなりの量のコードになります。双方向反復子には追加の 38 が必要です。 (2 つのオーバーロード)、およびランダム アクセス イテレータ 425567 (2 つのオーバーロード)、74 (3 つのオーバーロード),87 および完全な比較演算子 (4 つのオーバーロード、C++20 では 1 つ)。特に複数の反復子が必要な場合は、大量の入力が必要です。

ただし、作成した 6 つのメンバ関数のうち 94 に注意してください。 、 105110 128 の観点から完全に実装されています 、 137 、および 146 .それらの実装は、何も考えていない純粋なボイラープレートです。

それを自動化しましょう。

アプローチ #1:150 関数

基本的な考え方は、継承を使用し、必要なボイラープレートをコードに挿入する基本クラスを作成することです。ここでの唯一の問題は、派生クラスで定義された関数を呼び出す必要があることです。または、より正確に言うと、次の関数を呼び出す必要があります。署名は知られていますが、その実装は不明です。

これはまさに 169 です 関数は次のことを行うように設計されています:

template <typename ReferenceType>
struct forward_iterator_interface
{
    // To be implemented by the derived class.
    virtual ReferenceType operator*() const = 0;
    virtual forward_iterator_interface& operator++() = 0;
    virtual bool operator==(const forward_iterator_interface& other) const = 0;

    // The boilerplate.
    auto operator->() const
    {
        return &**this; // virtual call
    }

    void operator++(int)
    {
        ++*this; // virtual call
    }

    bool operator!=(const forward_iterator_interface& rhs) const
    {
        return !(*this == rhs); // virtual call
    }
};

template <typename Container>
class stable_iterator
: public forward_iterator_interface<const typename Container::value_type&>
{

…

public:
    reference_type operator*() const override
    {
        assert(_container && _index < _container->size());
        return (*_container)[_index];
    }

    // Note: we can return the derived type here.
    stable_iterator& operator++() override
    {
        assert(_container && _index < _container->size());
        ++_index;
        return *this;
    }
    // Need to pull-in the other overload of operator++.
    using forward_iterator_interface<reference_type>::operator++;

    bool operator==(const forward_iterator_interface<reference_type>& _rhs) const override
    {
        auto& rhs = dynamic_cast<const stable_iterator&>(_rhs);
        assert(_container == rhs._container);
        return _index == rhs._index;
    }
};

これは単純に思えます:基本クラス 174 を追加しました 派生クラスが純粋な 188 として実装する必要がある関数を宣言する 193 の署名として、テンプレート化する必要があることに注意してください。 (したがって 204 ) はイテレータの参照型に依存し、メンバー バージョンの 213 に切り替える必要がありました。 非メンバーはバーチャルになれません。

225 で 実装では、適切な参照型で基本クラスから継承し、必要な関数を実装します。ここでは 236 が必要です 249 のシャドウイングを防ぐ宣言 基本クラスのオーバーロード、および 253 263 で正しい型を取得する .

しかし、実際には 275 を実装できませんでした 正しくは、派生オブジェクトのコピーを返す必要がありますが、これはできません。まず、唯一の戻り値の型は 287 です。 、これは抽象クラスであるため、返すことはできません。また、できたとしても、オブジェクトの基本部分をスライスします。

この問題は、基本クラスが派生型で実際にテンプレート化されている CRTP を使用して解決できます。

アプローチ #2:CRTP

CRTP の背後にある考え方は、一部の基本クラスが派生クラスをテンプレート引数として受け取るというものです。このようにして、派生クラスの静的型は基本クラスの実装で認識されます。そのため、実際に <を使用する必要はありません。コード>296 代わりに、静的にダウンキャストして派生関数を直接呼び出すことができます。

template <typename Derived>
struct forward_iterator_interface
{
    auto operator->() const
    {
        // Downcast ourselves to the derived type.
        auto& derived = static_cast<const Derived&>(*this);
        return &*derived; // delegate
    }

    Derived operator++(int)
    {
        auto& derived = static_cast<const Derived&>(*this);

        Derived copy(derived);
        ++derived; // delegate
        return copy;
    }

    friend bool operator!=(const Derived& rhs, const Derived& lhs)
    {
        return !(lhs == rhs); // delegate
    }
};

template <typename Container>
class stable_iterator
: public forward_iterator_interface<stable_iterator<Container>>
{

…

public:
    reference_type operator*() const
    {
        assert(_container && _index < _container->size());
        return (*_container)[_index];
    }

    stable_iterator& operator++()
    {
        assert(_container && _index < _container->size());
        ++_index;
        return *this;
    }
    // Need to pull-in the other overload of operator++.
    using forward_iterator_interface<stable_iterator>::operator++;

    friend bool operator==(const stable_iterator& lhs,
                           const stable_iterator& rhs)
    {
        assert(lhs._container == rhs._container);
        return lhs._index == rhs._index;
    }
};

CRTP 基本クラスでは、301 を宣言する必要はありません。 function.314 で関数を呼び出すには 、必要なのは 328 をダウンキャストすることだけです 332 に type.これは完全に安全です:346 派生型なので、355 実際には 362 です ユーザーが失敗して間違った型を 379 に渡した場合 、これは問題ですが、ここに示すように、その型が CRTP 基本クラスからも継承する場合のみです。ユーザーがそれを継承しない型を渡すと、380 コンパイルされません。

390 として 400 から正しい型を返すためにインターフェイスで直接使用できます。 、そして 411 で正しいタイプを受け入れます – いいえ 428

437 の実装 元のものとほとんど同じですが、すべてのボイラープレートを自分で書く代わりに、448 から継承しました。 .まだ 454 が必要です ただし、宣言。

別のアプローチとして、名前 467 を使い続ける必要はありません 、 476489 490 のように名前を付けることができます。 、 501 、および 519 524 ですべてのイテレータ演算子を実装します そうすれば、537 は必要ありません。 派生クラスでの宣言。

さらに、545 また、イテレータの typedef を宣言することもできます。これらは同様に継承されるため、552

CRTP インターフェース技術

一般的なテクニックは次のとおりです。いくつかの基本クラス 569 があります。 派生クラスをテンプレート引数として受け取ります。次に、ダウンキャストを使用して派生クラスのメソッドを呼び出すことにより、いくつかのボイラープレート メソッドを実装します。ユーザー クラスは 579 から継承します。 必要なメソッドを実装します。その後、ボイラープレートを無料で取得します。

// Definition.
template <typename Derived>
class foo_interface
{
public:
    using some_type = int;

    void do_sth_twice()
    {
        // Access the derived object.
        auto& derived = static_cast<Derived&>(*this);
        // Call a member function of the derived object.
        derived.do_sth();
        derived.do_sth();
    }

    static int get_static_value()
    {
        // Call a static member function of the derived type.
        return compute(Derived::get_static_value_impl(), 42);
    }

private:
    // You can also inject members as necessary.
    int special_value;
};

// Usage.
class my_foo
: public foo_interface<my_foo>
{
public:
    void do_sth() { … }

private:
    // Implementation helper only.
    static int get_static_value_impl() { … }

    // The interface class needs to be able to call the helper.
    friend class foo_interface<my_foo>;
};

従来の継承と 583 との比較 関数の場合、CRTP インターフェイス手法は、派生型の型や静的関数にもアクセスできるため、より強力です。599 もありません。 関数呼び出しのオーバーヘッド。

派生型は、それ自体を実装するだけで、CRTP インターフェイスのデフォルトの実装をオーバーライドすることも選択できます。他のコードは派生型のみを使用するため、継承されたものをシャドウする新しい実装を呼び出します。たとえば、600 612 の実装を選択できます それ自体:

template <typename Container>
class stable_iterator
: public forward_iterator_interface<stable_iterator<Container>>
{
public:
    …

    // "Override" the inherited implementation of `operator->()`.
    auto operator->() const
    {
        // A potentially more efficient version or something.
        return _container->data() + _index;
    }
};

CRTP 基本クラス内のコードは、メソッドの「オーバーライドされた」バージョンを自動的に呼び出さないことに注意してください。これは、シャドウイングが行われないスコープ内で名前のルックアップが実行されるためです。オーバーライドを予測するには、基本クラスで呼び出しを修飾する必要があります。

template <typename Derived>
class foo_interface
{
public:
    // Assume that derived classes may override this one.
    void some_method() { … }

    void use_some_method()
    {
        // This always calls the base version of `some_method()`.
        some_method();

        // This might call a derived version of `some_method()`.
        static_cast<Derived&>(*this).some_method();
    }
};

この手法の一般的な問題は、型 624 が 基本クラスのクラス本体が解析されている間、不完全です:638 にアクセスしています メンバー関数定義の外側はコンパイルされません。

template <typename Derived>
class forward_iterator_interface
{
public:
    // Error: use of incomplete type `Derived`.
    using reference = const typename Derived::value_type&;

    // Error: use of incomplete type `Derived`.
    typename Derived::pointer operator->() const
    {
        auto& derived = static_cast<const Derived&>(*this);
        // OK, inside the body `Derived` is complete.
        typename Derived::pointer ptr = &*derived;
        return ptr;
    }
};

そのため、CRTP 基本クラスのメンバー関数には 641 が必要になる場合があります。 その時点で実際の型に名前を付けることができないため、型を返します。 658 の typedef にアクセスするには 666 など 上記の例では、追加のテンプレート パラメータが必要です。

template <typename Derived, typename ValueType>
class forward_iterator_interface
{
public:
    using reference = const ValueType&; // OK
};

template <typename Container>
class stable_iterator
: public forward_iterator_interface<stable_iterator<Container>,
            typename Container::value_type>
{
    …
};

結論

インターフェイス ボイラープレートを共有する複数の型を記述する必要がある場合は常に、代わりに CRTP インターフェイス手法を検討してください。これにより、ボイラープレートを一度実装するだけで、単純な継承によってすべての型に自動的に追加できます。

この手法の実際のアプリケーションには次のようなものがあります:

  • lexy による 671 の実装 (685 と呼ばれます)
  • 一般的な 695 を含む Boost.STLInterfaces ここで実装したように (特に)
  • C++20 の 701 、ビュー タイプのボイラープレートを排除します。
  • type_safe の強力な typedef 実装。