tuple_iterator の実装

この投稿は、Arne Mertz とのコラボレーションの一部です。 Arne は Zühlke のソフトウェア エンジニアであり、最新の C++ に重点を置いたクリーン コードの愛好家です。彼はオンラインの Twitter と彼の「Simplify C++!」で見つけることができます。ブログです。std::tuple へのアクセスについて書いています。 、しかし、私たちのブログを交換しました - 私の投稿は彼のブログで終わり、彼の投稿はここに続きます:

std::tuple の内容をどのように繰り返し処理できるか疑問に思ったことはありませんか? 実行時、配列または std::vector と同様 ?このような機能の必要性に気付く場合もあれば、気付かない場合もあります。このウォークスルーでは、概念実証と、C++17 でこのような問題に取り組む方法を示します。

ミッション

「タプルの内容を繰り返し処理する」というと、範囲ベースの for ループを思い浮かべます。タプルには begin() もありません と end() また、名前空間 std でこれらの関数の無料バージョンをオーバーロードすることも許可されていません .つまり、タプルを直接超える範囲ベースは不可能であるため、std::tuple の周りに機能のラッパーを提供する必要があります。 .

もう 1 つの問題は、繰り返し処理するコンテンツです。これは std::tuple のインスタンス化に対して機能するはずです。 、つまり、任意の内容で。繰り返し処理する要素は、ある種の合計型でなければなりません。 STL での型は std::variant です 、および std::visit 付き その中にあるものは何でもアクセスできます。

動作させたいコードの例は次のとおりです:

int main() {
 std::tuple<int, std::string, double> tup{42, "foo", 3.14};
 for (auto const& elem : to_range(tup)) { 
 std::visit(
 overload(
 [](int i) { std::cout << "int: " << i << '\n'; },
 [](std::string const& s) { std::cout << "string: " << s << '\n'; },
 [](double d) { std::cout << "double: " << d << '\n'; }
 ),
 elem
 );
 }
}

ここで、overload すべての引数を 1 つの関数オブジェクトにまとめる機能です。

分解

実行時のコンパイル時アクセス?

コンパイル時にタプルを反復処理するのは簡単です。 std::get<N> で どのメンバーにもアクセスできます。 N ただし、コンパイル時に認識する必要があります。範囲ベースの for ループの反復子がすべてのステップで型を変更できる場合、 tuple_iterator<N> と書くだけで済みます。 テンプレートを作成して終了です。

しかし、そう簡単ではありません。反復は実行時に行われ、タプルへの任意の実行時アクセスはありません。つまり、何らかの形で実行時情報 (つまり、反復子が指す要素) を、コンパイル時間情報を必要とするアクセス関数にマップする必要があります。

これを実現する唯一の方法は、コンパイル時のすべての情報を、実行時に繰り返し処理できるリストに入れることです。つまり、ルックアップ テーブルが必要です。

template< /* ??? */ >
struct tuple_runtime_access_table {
 using tuple_type = /* ??? */;
 using return_type = /* ??? */;
 using converter_fun = /* ??? */;

 template <std::size_t N>
 static return_type access_tuple(tuple_type& t, converter_fun& f) {
 return f(std::get<N>(t));
 }

 using accessor_fun_ptr = return_type(*)(tuple_type&, converter_fun&);
 const static auto table_size = std::tuple_size_v<tuple_type>;

 const static std::array<accessor_fun_ptr, table_size> lookup_table = {
 {&access_tuple<0>, &access_tuple<1>, /* ... and so on ... */ , &access_tuple<table_size - 1> }
 };
};

これを段階的に見ていきましょう:std::get<N> 以降 さまざまな型を返します。std::get<0> のアドレスを単純に取得することはできません 、 std::get<1> など、特定のタプルについて。結果を result_type に変換する必要があります これらすべての機能に共通です。 std::variant 前に言いました。

それを取得するには、converter_fun が必要です タプルの任意の要素に適用される関数または関数オブジェクトは、result_type になります .静的関数テンプレート access_tuple<N> まさにこれを行います。最後になりましたが、これらすべての関数へのポインタをルックアップ テーブルに詰め込む必要があります。

空白を埋める

この 1 つのテンプレートにあまり多くのロジックを入れたくないので、tuple_type のテンプレート パラメーターを使用できます。 、 return_typeconverter_fun .さらに、テーブルの内容を生成するには、0 から table_size -1 までのインデックスを生成する必要があります ここに示すように。これは、可変長非型テンプレートの典型的な使用例です。

template <typename Tup, typename R, typename F, std::size_t... Idxs>
struct tuple_runtime_access_table {
 using tuple_type = Tup;
 using return_type = R;
 using converter_fun = F;

 template <std::size_t N>
 static return_type access_tuple(tuple_type& t, converter_fun& f) {
 return f(std::get<N>(t));
 }

 using accessor_fun_ptr = return_type(*)(tuple_type&, converter_fun&);
 const static auto table_size = sizeof...(Idxs);

 constexpr static std::array<accessor_fun_ptr, table_size> lookup_table = {
 {&access_tuple<Idxs>...}
 };
};

レバレッジ タイプ控除

特にコンバーター関数はおそらくラムダであるため、ほとんどのテンプレート パラメーターを推定したいと考えています。インデックス パラメータ パックは std::index_sequence 経由で提供されます .それでは、型推論を行うための小さなユーティリティ関数を書きましょう:

template <typename R, typename Tup, typename F, std::size_t... Idxs>
auto call_access_function(Tup& t, std::size_t i, F f, std::index_sequence<Idxs...>) {
 auto& table = tuple_runtime_access_table<Tup, R, F, Idxs...>::lookup_table;
 auto* access_function = table[i];
 return access_function(t, f);
}

ここで、明示的に指定する必要があるのは戻り値の型だけです。 R でもないことに注意してください F でもありません 、 Idxs... でもありません この時点で指定されています。つまり、インデックス リスト内のすべての要素に適用でき、戻り値の型が R に変換可能である限り、これを使用して任意の F をタプルで実行できます。

戻り値の型

その戻り値の型をより具体的にする時が来ました。私はそれが std::variant であるべきだと書きました .タプルへの書き込みアクセスを可能にし、コストがかかる可能性のあるタプル要素のコピーを作成する必要がないようにするために、variant 参照を含める必要があります。悲しいことに、std::variant 参照が含まれていない可能性があるため、std::reference_wrapper を使用する必要があります .

template <typename Tup> struct common_tuple_access;

template <typename... Ts>
struct common_tuple_access<std::tuple<Ts...>> {
 using type = std::variant<std::reference_wrapper<Ts>...>;
};

標準ライブラリは、std::tuple で利用できるほとんどの機能を提供する努力をしています。 std::pair にも と std::array .したがって、このメタ関数もこれら 2 つに特化する必要があります。 std::array の場合は注意してください すでに begin() があるため、ほとんどの場合、これはまったく役に立ちません。 と end() メンバー関数。

template <typename T1, typename T2>
struct common_tuple_access<std::pair<T1, T2>> {
 using type = std::variant<std::reference_wrapper<T1>, std::reference_wrapper<T2>>;
};

template <typename T, auto N>
struct common_tuple_access<std::array<T, N>> {
 using type = std::variant<std::reference_wrapper<T>>;
};

そして最後に、簡単にアクセスできるようにします。

template <typename Tup>
using common_tuple_access_t = typename common_tuple_access<Tup>::type;

ランタイム アクセス関数

ルックアップ テーブルとユーティリティ関数を使用すると、単純にその N 番目のエントリを取り、それをタプルで呼び出して std::variant を取得する関数を記述できるはずです。 対応する要素が含まれています。欠けているのは、ラッピングを行う関数オブジェクトを std::reference_wrapper に書き込むことだけです。 適切な std::index_sequence を作成してください :

template <typename Tup>
auto runtime_get(Tup& t, std::size_t i) {
 return call_access_function<common_tuple_access_t<Tup>>(
 t, i, 
 [](auto & element){ return std::ref(element); },
 std::make_index_sequence<std::tuple_size_v<Tup>>{}
 );
}

後は簡単です…

i へのランタイム アクセスに取り組みました 任意のタプルの th 要素であり、範囲ベースの for ループへの残りの部分は比較的単純です。

tuple_iterator

範囲ベースの for ループの絶対最小値は、begin() から返される反復子の型です。 プリインクリメント演算子と逆参照演算子が定義されており、その operator!= begin() によって返される 2 つの型に対して定義されます。 と end() . C++17 からは、2 つの型が必ずしも同じである必要はないことに注意してください。

begin() に同じタイプのイテレータを使用すれば、この目的には十分です。 と end() .個人的には operator!= だと思います 常に operator== の観点から実装する必要があります 、可能であれば、それも提供します。

template <typename Tup> class tuple_iterator {
 Tup& t;
 size_t i;
public:
 tuple_iterator(Tup& tup, size_t idx)
 : t{tup}, i{idx} 
 {}
 
 tuple_iterator& operator++() { 
 ++i; return *this; 
 }
 bool operator==(tuple_iterator const& other) const {
 return std::addressof(other.t) == std::addressof(t)
 && other.i == i;
 }
 
 bool operator!=(tuple_iterator const& other) const {
 return !(*this == other);
 }

 auto operator*() const{ 
 return runtime_get(t, i); 
 }
};

これを適切なイテレータにするために実装することは他にもたくさんあります。範囲チェックと他の多くの演算子がありますが、それは読者の課題として残します.

to_range

パズルの最後のピースは、非常に単純な範囲ラッパーです:

template <typename Tup>
class to_range {
 Tup& t;
public: 
 to_range(Tup& tup) : t{tup}{}

 auto begin() {
 return tuple_iterator{t, 0};
 }
 auto end() {
 return tuple_iterator{t, std::tuple_size_v<Tup>};
 }
 
 auto operator[](std::size_t i){
 return runtime_get(t, i);
 }
};

繰り返しますが、必要な操作と operator[] のオーバーロードのみを提供します。 単一要素へのアクセスを容易にする

オーバーロード

クラスのテンプレート推定を使用すると、オーバーロードは C++17 で比較的単純かつ単純に実装できます。

template <class ... Fs>
struct overload : Fs... {
 overload(Fs&&... fs) : Fs{fs}... {}
 using Fs::operator()...;
};

後の標準にもっと洗練されたものを追加するという提案もありますが、この使用例ではそれで十分です.

すべてをまとめる

元の目標をもう一度見てみましょう:

int main() {
 std::tuple<int, std::string, double> tup{42, "foo", 3.14};
 for (auto const& elem : to_range(tup)) { 
 std::visit(
 overload(
 [](int i) { std::cout << "int: " << i << '\n'; },
 [](std::string const& s) { std::cout << "string: " << s << '\n'; },
 [](double d) { std::cout << "double: " << d << '\n'; }
 ),
 elem
 );
 }
}

このコードはそのままコンパイルされ、期待される結果が得られます。また、std::pair に対しても「問題なく動作」します。 、 common_tuple_access を処理したため

reference_wrapper の処理

std::reference_wrapper を使用するというトレードオフが必要だったので バリアント内では、その事実に注意する必要があります。たとえば、ビジターにジェネリック ラムダがある場合、常に reference_wrappers で呼び出されます。 意図した機能の代わりに.

さらに、参照ラッパーに std::string のようなテンプレートが含まれている場合 、次に operator<< 経由で印刷します std::reference_wrapper<std::string>> からの暗黙の変換を考慮しないため、失敗します。 std::string へ .したがって、次のコードはテンプレート エラー小説になります:

…
std::visit(
 overload(
 [](int i) { std::cout << "int: " << i << '\n'; },
 [](double d) { std::cout << "double: " << d << '\n'; },
 [](auto const& s) { std::cout << "???: " << s << '\n'; }
 ),
 elem
);
…

これは、オーバーロードから派生し、アンラップを適用するヘルパーで修正できます:

template <class ... Fs>
struct overload_unref : overload<Fs...> {
 overload_unref(Fs&&... fs) 
 : overload<Fs...>{std::forward<Fs>(fs)...} 
 {}

 using overload<Fs...>::operator();

 template <class T>
 auto operator()(std::reference_wrapper<T> rw){
 return (*this)(rw.get());
 }
};

これを使用すると、コードは再び機能します:

int main() {
 std::tuple<int, std::string, double> tup{42, "foo", 3.14};
 for (auto const& elem : to_range(tup)) { 
 std::visit(
 overload_unref(
 [](int i) { std::cout << "int: " << i << '\n'; },
 [](double d) { std::cout << "double: " << d << '\n'; },
 [](auto const& s) { std::cout << "???: " << s << '\n'; }
 ),
 elem
 );
 }
}

完全なコードは GitHub にあります。

結論

いくらかのオーバーヘッドが伴いますが、タプルへのランタイム アクセスを取得できます。関数ポインタ テーブルを介したリダイレクトは最適化されず、std::visit のバリアントのコンテンツの解決もできません。 .コンパイル時にどの要素にアクセスしているかを知る必要がないため、ある程度のパフォーマンスと柔軟性を犠牲にしています。

operator[] を実装する方法を知りたいですか? 不器用な std::get<N> を作ることができます 実行時間のオーバーヘッドなしでタプルを呼び出す方がはるかに優れていますか?Jonathan のソリューションについては、私のブログにアクセスしてください!