STLのようなインターフェイスを備えたカスタムコンテナを実装しています。通常のイテレータとconstイテレータを提供する必要があります。 2つのバージョンのイテレータのコードのほとんどは同一です。この重複を回避するにはどうすればよいですか?
たとえば、私のコンテナクラスはFoo
で、FooIterator
とFooConstIterator
を実装しています。両方のイテレータは、同一のoperator++()
のようなメソッドを提供する必要があります。
私の質問は 同様のconstメンバー関数とnon-constメンバー関数の間のコードの重複を削除するにはどうすればよいですか? に似ていますが、その答えはconstメソッドとnon-constメソッド、特にアクセサーに固有です。それがイテレータの問題にどのように一般化するのかわかりません。
FooIterator
をFooConstIterator
から派生させ、追加の非constメソッドで拡張する必要がありますか?これは、仮想メソッドまたはメソッドの非表示につながりますが、ここでは不適切と思われます。
おそらく、FooIterator
にはFooConstIterator
が含まれている必要があります。このアプローチは実装の重複を減らしますが、多くの定型メソッド定義を再導入しているようです。
単一の定義から2つのイテレーターを生成するための巧妙なテンプレート手法はありますか?あるいは、プリプロセッサを使用してこれらのほぼ同一のクラスをスタンプアウトする方法があります。
ローカルのSTL実装を調べて、これがどのように処理されるかを確認してみました。ヘルパークラスが多すぎてデザインを練るのに苦労していますが、機能が単純に複製されているようです。
以前のプロジェクトでは、カスタムコンテナは標準のSTLコンテナの上に構築されていたため、独自のイテレータを用意する必要はありませんでした。この場合、それはオプションではありません。
[残念ながら、ベストアンサーはリンクのみのアンサーであったため、モデレーターによって削除されました。リンクのみの回答が推奨されない理由を理解しています。しかし、それを削除することは、将来の探求者から非常に有用な情報を奪いました。リンクは7年以上安定しており、この記事の執筆時点では引き続き機能します。]
2001年1月のMattAusternによる元のDr.Dobb's Journalの記事 "The Standard Librarian:Defining Iterators and Const Iterators" を強くお勧めします。 、それも利用可能です ここ 。
この置換回答が削除されないようにするために、解決策を要約します。
アイデアは、追加のテンプレートパラメータ(これがconstバージョンであるかどうかを示すブール値)を受け取るテンプレートとしてイテレータを一度実装することです。 constバージョンとnon-constバージョンが異なる実装のどこでも、テンプレートメカニズムを使用して正しいコードを選択します。 MattAusternのメカニズムはchoose
と呼ばれていました。それはこのように見えました:
template <bool flag, class IsTrue, class IsFalse>
struct choose;
template <class IsTrue, class IsFalse>
struct choose<true, IsTrue, IsFalse> {
typedef IsTrue type;
};
template <class IsTrue, class IsFalse>
struct choose<false, IsTrue, IsFalse> {
typedef IsFalse type;
};
Constイテレータとnon-constイテレータに別々の実装がある場合、const実装には次のようなtypedefが含まれます。
typedef const T &reference;
typedef const T *pointer;
非const実装は次のようになります。
typedef T &reference;
typedef T *pointer;
ただし、choose
を使用すると、追加のテンプレートパラメータに基づいて選択する単一の実装を作成できます。
typedef typename choose<is_const, const T &, T &>::type reference;
typedef typename choose<is_const, const T *, T *>::type pointer;
基になる型にtypedefを使用することにより、すべてのイテレーターメソッドに同一の実装を持たせることができます。 Matt Austernの 完全な例 を参照してください。
C++ 11/14以降、このような小さなヘルパーを回避して、ブールテンプレートから直接定数を推測することができます。
constness.h:
#ifndef ITERATOR_H
#define ITERATOR_H
#include <cstddef>
#include <cstdint>
#include <type_traits>
#include <iterator>
struct dummy_struct {
int hello = 1;
int world = 2;
dummy_struct() : hello{ 0 }, world{ 1 }{ }
};
template< class T >
class iterable {
public:
template< bool Const = false >
class my_iterator {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = T;
using difference_type = std::ptrdiff_t;
/* deduce const qualifier from bool Const parameter */
using reference = typename std::conditional_t< Const, T const &, T & >;
using pointer = typename std::conditional_t< Const, T const *, T * >;
protected:
pointer i;
public:
my_iterator( T* _i ) : i{ reinterpret_cast< pointer >( _i ) } { }
/* SFINAE enables the const dereference operator or the non
const variant
depending on bool Const parameter */
template< bool _Const = Const >
std::enable_if_t< _Const, reference >
operator*() const {
std::cout << "Const operator*: ";
return *i;
}
template< bool _Const = Const >
std::enable_if_t< !_Const, reference >
operator*() {
std::cout << "Non-Const operator*: ";
return *i;
}
my_iterator & operator++() {
++i;
return *this;
}
bool operator!=( my_iterator const & _other ) const {
return i != _other.i;
}
bool operator==( my_iterator const & _other ) const {
return !( *this != _other );
}
};
private:
T* __begin;
T* __end;
public:
explicit iterable( T* _begin, std::size_t _count ): __begin{ _begin }, __end{ _begin + _count } { std::cout << "End: " << __end << "\n"; }
auto begin() const { return my_iterator< false >{ __begin }; }
auto end() const { return my_iterator< false >{ __end }; }
auto cbegin() const { return my_iterator< true >{ __begin }; }
auto cend() const { return my_iterator< true >{ __end }; }
};
#endif
これは、次のようなもので使用できます。
#include <iostream>
#include <array>
#include "constness.h"
int main() {
dummy_struct * data = new dummy_struct[ 5 ];
for( int i = 0; i < 5; ++i ) {
data[i].hello = i;
data[i].world = i+1;
}
iterable< dummy_struct > i( data, 5 );
using iter = typename iterable< dummy_struct >::my_iterator< false >;
using citer = typename iterable< dummy_struct >::my_iterator< true >;
for( iter it = i.begin(); it != i.end(); ++it ) {
std::cout << "Hello: " << (*it).hello << "\n"
<< "World: " << (*it).world << "\n";
}
for( citer it = i.cbegin(); it != i.cend(); ++it ) {
std::cout << "Hello: " << (*it).hello << "\n"
<< "World: " << (*it).world << "\n";
}
delete[] data;
}
STLは継承を使用します
template<class _Myvec>
class _Vector_iterator
: public _Vector_const_iterator<_Myvec>
恒常性と非恒常性をテンプレート化する可能性があるという提案に加えて、同じ解決策についても言及している Boost.Iteratorチュートリアル -を見て作業量を減らすこともできます。
CRTPと共通ベースを使用してメソッドを「注入」するか(ただし、現在のC++ではctorを複製する必要があります)、またはプリプロセッサを使用するだけです(震える必要はなく、ctorを簡単に処理できます)。
struct Container {
#define G(This) \
This operator++(int) { This copy (*this); ++*this; return copy; }
// example of postfix++ delegating to ++prefix
struct iterator : std::iterator<...> {
iterator& operator++();
G(iterator)
};
struct const_iterator : std::iterator<...> {
const_iterator& operator++();
G(const_iterator)
};
#undef G
// G is "nicely" scoped and treated as an implementation detail
};
Std :: iterator、それが提供するtypedef、およびマクロを単純にするために提供する可能性のあるその他のtypedefを使用します。
Arthor O'Dwyerは、彼のブログ投稿でこれに詳細に回答しています: https://quuxplusone.github.io/blog/2018/12/01/const-iterator-antipatterns/
本質的に、
_template<bool IsConst>
class MyIterator {
int *d_;
public:
MyIterator(const MyIterator&) = default; // REDUNDANT BUT GOOD STYLE
template<bool IsConst_ = IsConst, class = std::enable_if_t<IsConst_>>
MyIterator(const MyIterator<false>& rhs) : d_(rhs.d_) {} // OK
};
using Iterator = MyIterator<false>;
using ConstIterator = MyIterator<true>;
};
_
また、コードにstatic_assert(std::is_trivially_copy_constructible_v<ConstIterator>);
を追加して、イテレータが簡単にコピーできるようにします。
結論:独自のコンテナーイテレーター、またはこの「一方向の暗黙的な変換」動作を備えた他のタイプのペア(Networking TSのconst_buffers_typeやmutable_buffers_typeなど)を実装する場合は、上記のパターンの1つを使用して変換コンストラクターを実装する必要があります。 些細なコピー可能性を誤って無効にすることなく。