C ++ 17で異種コンテナーを操作する

こんにちは、Habr! 最近、特にロシアの国家標準化ワーキンググループの出現で、C ++ 17について多くのことが言われています。 問題のないネットワークのオープンスペースでは、最新のC ++標準を使用した短い例を見つけることができます。 すべては問題ありませんが、新しい標準への真に広範な移行は見られません。 したがって、最低14の標準を必要とするライブラリはすべて、事後的に最新と見なされている写真を観察できます。



この出版物では、実行時の異種コンテナを使用した便利な作業のために、小さなライブラリ(3つの関数( applyfilterreduce )と1つが「宿題」( map ):)を開発します



新しいライブラリの種類に加えて、新しいものから、 折り畳み式の味と、ほとんど構造化されていないバインディングを試してみます



はじめに



はじめに、異種コンテナのトピックの簡単な紹介。 ご存知のように、c ++のランタイムで実行される実際の異種コンテナーはありません。 std :: tupleを自由に使用できます。その痕跡は、実行時にほとんど完全に消えます(使用しないものにお金を払わない)... ...それだけです。 他のすべては、あなた自身の図書館自転車を作るためのただの積み木です。



異種コンテナを作成する2つのビルディングブロック、 std :: anystd :: variantがあります。 最初のものはタイプを覚えていないため、その使用は非常に限られています。 std :: variantは型を記憶し、 std :: visitメソッドのテーブルとそれに続く遷移を生成することで実装されます)を使用して現在の型にファンクターを一致させる方法を知っています。 実装は本当に魔法であり、一見して不可能なことを行うのに役立つのは魔法だけです:)(もちろん、c ++ではすべてが可能であるため可能です)。 std ::バリアントの内部は、ブーストバージョンを標準に移行するためにオーバーヘッドがあまりなく、開発者は(それが何であるかに関して)パフォーマンスを処理しました。 要約すると、 std :: variantは型のコンテナであり、異種コンテナの基本単位です。



免責事項



コードの最大のコンパクトさについて事前に警告します。 それを不注意にコピーしないでください、それは迅速な理解のために可能な限り簡単でした。 名前空間やリンク転送などはありません。



私はまた、ユニークであるふりもしません、確かに似たような良いライブラリがあるでしょう:)



開始する



関数の理解とテストを簡単にするために、簡単な例を見てみましょう。 これを行うには、通常の多態性構造をエミュレートします。



struct Circle { void Print() { cout << "Circle. " << "Radius: " << radius << endl; } double Area() { return 3.14 * radius * radius; } double radius; }; struct Square { void Print() { cout << "Square. Side: " << side << endl; } double Area() { return side * side; } double side; }; struct EquilateralTriangle { void Print() { cout << "EquilateralTriangle. Side: " << side << endl; } double Area() { return (sqrt(3) / 4) * (side * side); } double side; }; using Shape = variant<Circle, Square, EquilateralTriangle>;
      
      





また、比較のために、その単純な多相類似体を念頭に置いてください。



 struct Shape { virtual void Print() = 0; virtual double Area() = 0; virtual ~Shape() {}; }; struct Circle : Shape { Circle(double val) : radius(val) {} void Print() override { cout << "Circle. " << "Radius: " << radius << endl; } double Area() override { return 3.14 * radius * radius; } double radius; }; struct Square : Shape { Square(double val) : side(val) {} void Print() override { cout << "Square. Side: " << side << endl; } double Area() override { return side * side; } double side; }; struct EquilateralTriangle : Shape { EquilateralTriangle(double val) : side(val) {} void Print() override { cout << "EquilateralTriangle. Side: " << side << endl; } double Area() override { return (sqrt(3) / 4) * (side * side); } double side; };
      
      





ベクトルを作成し、標準ツールを使用して多態的な動作を実現してみましょう。 ベクターによってテストされ、 Print関数を呼び出します。



最初に、(仮想関数で)動的なアナログを使用します。 ご想像のとおり、動的ポリモーフィズムに問題はありません。



 vector<Shape*> shapes; shapes.emplace_back(new Square(8.2)); shapes.emplace_back(new Circle(3.1)); shapes.emplace_back(new Square(1.8)); shapes.emplace_back(new EquilateralTriangle(10.4)); shapes.emplace_back(new Circle(5.7)); shapes.emplace_back(new Square(2.9));
      
      





ただし、非常にモダンに見えません。 新しいものへのむき出しの呼び出しは自信を刺激しません。 書き直します:



 vector<shared_ptr<Shape>> shapes; shapes.emplace_back(make_shared<Square>(8.2)); shapes.emplace_back(make_shared<Circle>(3.1)); shapes.emplace_back(make_shared<Square>(1.8)); shapes.emplace_back(make_shared<EquilateralTriangle>(10.4)); shapes.emplace_back(make_shared<Circle>(5.7)); shapes.emplace_back(make_shared<Square>(2.9));
      
      





今では良くなりました。 ただし、初心者にとっては、コードは明らかに増加しませんでした。 しかし、私たちはホリバーを繁殖させません、私たちの仕事を果たします:



 for (shared_ptr<Shape> shape: shapes) { shape->Print(); } // : // Square. Side: 8.2 // Circle. Radius: 3.1 // Square. Side: 1.8 // EquilateralTriangle. Side: 10.4 // Circle. Radius: 5.7 // Square. Side: 2.9
      
      





また、異種コンテナに対して同様の動作を実装してみてください。



 vector<Shape> shapes; shapes.emplace_back(EquilateralTriangle { 5.6 }); shapes.emplace_back(Square { 8.2 }); shapes.emplace_back(Circle { 3.1 }); shapes.emplace_back(Square { 1.8 }); shapes.emplace_back(EquilateralTriangle { 10.4 }); shapes.emplace_back(Circle { 5.7 }); shapes.emplace_back(Square { 2.9 });
      
      





ここにはポインタはありません。 問題なく、スタック上のオブジェクトを操作できます。 また、コストラクタの代わりに、「中程度に単純な」型の集約初期化を使用できます。



ただし、単純に繰り返して関数を呼び出すことはできません。 std :: variantが提供するツールを使用して、これを実行してみましょう。 これを行うには、関数std :: visitがあり 、ファンクターのクラスも作成する必要があります。



すべては次のようになります。



 struct Visitor { void operator()(Circle& c) { c.Print(); } void operator()(Square& c) { c.Print(); } void operator()(EquilateralTriangle& c) { c.Print(); } }; ... ... ... for (Shape& shape: shapes) { visit(Visitor{}, shape); }
      
      





結論は似ています。 また、constexpr ifを使用して同じ動作をエミュレートできます。 ここで誰かがすでに好きなもの。



標準ライブラリが提供する機能に慣れてきたので、異種シーケンスの作業を少し簡略化しようとします。



最も一般的で包括的な機能、 applyfilterreduceを実装します



ステップ1



まず、タスクを単純化します。 最初のステップは非常に原始的です-それは複数回説明されています。

可変長テンプレート、継承メカニズム、およびラムダ関数が通常の構造(ファンクター)に拡張されるという知識を活用してください。 ラムダセットを見て、テンプレートタイプの導出に役立つ関数を作成しましょう。



 template < typename... Func > class Visitor : Func... { using Func::operator()...; } template < class... Func > make_visitor(Func...) -> Visitor < Func... >;
      
      





これで、ファンクターを使用してクラスを作成する代わりに、署名に従って一致する一連のラムダを使用できます。



 for (Shape& shape: shapes) { visit(make_visitor( [](Circle& c) { c.Print(); }, [](Square& c) { c.Print(); }, [](EquilateralTriangle& c) { c.Print(); } ), shape); }
      
      





ジェネリックパラメーターで型推論を使用することもできます。



 for (Shape& shape: shapes) { visit(make_visitor([](auto& c) { c.Print(); }), shape); }
      
      





それはかなり良いと適度に短いことが判明しました。



適用する



すべてをまとめて収集し、異種シーケンスの適用機能を取得することは残ります。



 template < typename InputIter, typename InputSentinelIter, typename... Callable > void apply(InputIter beg, InputSentinelIter end, Callable... funcs) { for (auto _it = beg; _it != end; ++_it) visit(make_visitor(funcs...), *_it); };
      
      





できた 示されている手法は新しいふりをするものではなく、ブースト::バリアントを何らかの形で使用した開発者は、長い間このようなものを実装しています http://en.cppreference.com/w/cpp/utility/variant/visit、https:// habrahabr .ru / post / 270689 / )。



これで、同様の方法で関数を使用できます。



 apply(shapes.begin(), shapes.end(), [](auto& shape) { shape.Print(); });
      
      





または



 apply(shapes.begin(), shapes.end(), [] (Circle& shape) { shape.Print(); }, [] (Square& shape) { shape.Print(); }, [] (EquilateralTriangle& shape) { shape.Print(); });
      
      





ご覧のとおり、かなりクールでした。 ただし、 std :: variantにあるすべての型ではなくファンクターを渡すと、コンパイルエラーが発生します。 これを回避するために、SFINAE と同様に、他の代替手段がない場合に呼び出される省略記号付きのファンクターを作成し、呼び出しの順序で最新のオプションにします。



 template < typename InputIter, typename InputSentinelIter, typename... Callable > void apply(InputIter beg, InputSentinelIter end, Callable... funcs) { for (auto _it = beg; _it != end; ++_it) visit(make_visitor(funcs..., [](...){}), *_it); };
      
      





これで、すべてのタイプではなくファンクターを渡すことができます;存在しない場合、空のラムダが呼び出されます:



 //      Circle apply(shapes.begin(), shapes.end(), [] (Circle& shape) { shape.Print(); });
      
      





良い例として、動的ポリモーフィズムを使用してこれを行う方法を示します。



 //      Circle for_each(shapes.begin(), shapes.end(), [] (shared_ptr<Shape> shape) { if (dynamic_pointer_cast<Circle>(shape)) shape->Print(); });
      
      





最も快適な景色ではありません。



フィルター



類推により、 フィルター関数を作成します 。 署名に省略記号を含むラムダがbool型の値を返す必要があることを除いて、セマンティックロードは実際に違いはありません。 特定の型を処理するファンクターを渡さなかった場合、フィルターされたコンテナー内のインスタンスを表示したくないと想定します。



 template < typename InputIter, typename InputSentinelIter, typename OutputIter, typename... Callable > void filter(InputIter beg, InputSentinelIter end, OutputIter out, Callable... funcs) { for (auto _it = beg; _it != end; ++_it) { if (visit(make_visitor(funcs..., [] (...) { return false; }), *_it)) *out++ = *_it; } };
      
      





実装された関数は次のように使用できます。



 vector<Shape> filtered; filter(shapes.begin(), shapes.end(), back_inserter(filtered), [] (Circle& c) { return c.radius > 4.; }, [] (Square& s) { return s.side < 5.; }); apply(filtered.begin(), filtered.end(), [](auto& shape) { shape.Print(); }); // : // Square. Side: 1.8 // Circle. Radius: 5.7 // Square. Side: 2.9
      
      





ダイナミックポリモーフィズムを使用して実装されたアナログ:



 vector<shared_ptr<Shape>> filtered; copy_if(shapes.begin(), shapes.end(), back_inserter(filtered), [] (shared_ptr<Shape> shape) { if (auto circle = dynamic_pointer_cast<Circle>(shape)) { return circle->radius > 4.; } else if (auto square = dynamic_pointer_cast<Square>(shape)) { return square->side < 5.; } else return false; }); for_each(filtered.begin(), filtered.end(), [](shared_ptr<Shape> shape) { shape->Print(); }); // : // Square. Side: 1.8 // Circle. Radius: 5.7 // Square. Side: 2.9
      
      





減らす



reducestd :: analogのアナログ)とmapstd :: transformのアナログ)を実装することは残っています。 これらの関数の実装は、 applyおよびfilterを使用した場合よりもやや複雑です。 削減するために、2つのパラメーター(バッテリー値とオブジェクト自体)を持つファンクターを使用します。 同様の動作を実装するために、1つの引数の関数がstd :: variantに残るように、ラムダ関数を部分的に適用できます。 部分的に使用するC ++の美しいソリューションはありません。別のラムダを使用して必要なコンテキストをキャプチャする簡単な方法です。 単一のラムダではなく、 可変個のパックで作業していることを考えると、コードは肥大化し、読みにくくなり始めます。 折り畳み式を使用してバリアドを処理することで節約できます。 退役軍人は、以前にタイプリストを折りたたむ必要があった松葉杖を知っています。



 template < typename InputIter, typename InputSentinelIter, typename AccType, typename... Callable > struct reduce < InputIter, InputSentinelIter, AccType, false, Callable... > { constexpr auto operator()(InputIter beg, InputSentinelIter end, AccType initial_acc, Callable... funcs) { for (auto _it = beg; _it != end; ++_it) { initial_acc = visit(utility::make_overloaded_from_tup( tup_funcs(initial_acc, funcs...), make_index_sequence<sizeof...(Callable)>{}, [&initial_acc] (...) { return initial_acc; } ), *_it); } return initial_acc; } };
      
      





このようなことをするために、古き良きタプル( std :: tuple )を使用することが決定されました。 その要素の処理はそれほど複雑ではなく、いつでも独自のものを書くことができます。 そのため、バッテリー値をキャプチャして各ラムダを別のラムダでラップすることにより、新しいタプルに変換するラムダタプルを作成します。 新しい標準を使用したタプルの変換の利点は、比較的簡単に記述できます。



 template < typename... Types, typename Func, size_t... I > constexpr auto tuple_transform_impl(tuple<Types...> t, Func func, index_sequence<I...>) { return make_tuple(func(get<I>(t)...)); } template < typename... Types, typename Func > constexpr auto tuple_transform(tuple<Types...> t, Func f) { return tuple_transform_impl(t, f make_index_sequence<sizeof...(Types)>{}); }
      
      





外側のラムダを作成するには、着信ラムダの2番目の引数の型を知る必要があります。 インターネットにあるhelper'ovを使用して、ラムダを呼び出し演算子を持つ構造にキャストし、一致することで目的の型を取得できます。



すべて次のようになります。



 template < typename Func, typename Ret, typename _, typename A, typename... Rest > A _sec_arg_hlpr(Ret (Func::*)(_, A, Rest...)); template < typename Func > using second_argument = decltype(_sec_arg_hlpr(&Func::operator())); template < typename AccType, typename... Callable > constexpr auto tup_funcs(AccType initial_acc, Callable... funcs) { return tuple_transform(tuple<Callable...>{ funcs... }, [&initial_acc](auto func) { return [&initial_acc, &func] (second_argument<decltype(func)> arg) { return func(initial_acc, arg); }; }); }
      
      





すべては問題ありませんが、これらの不思議は、定義によって取得できない入力引数のタイプである汎用関数では機能しません。 したがって、 タグのディスパッチを使用して単純な特性を作成して関数をテストし、このケースの実装を作成します。

要約すると、 reduceに使用する次のオプションがあります。



 using ShapeCountT = tuple<size_t, size_t, size_t>; auto result = reduce(shapes.begin(), shapes.end(), ShapeCountT{}, [] (ShapeCountT acc, Circle& item) { auto [cir, sq, tr] = acc; return make_tuple(++cir, sq, tr); }, [] (ShapeCountT acc, Square& item) { auto [cir, sq, tr] = acc; return make_tuple(cir, ++sq, tr); }, [] (ShapeCountT acc, EquilateralTriangle& item) { auto [cir, sq, tr] = acc; return make_tuple(cir, sq, ++tr); }); auto [cir, sq, tr] = result; cout << "Circle count: " << cir << "\tSquare count: " << sq << "\tTriangle count: " << tr << endl; // : // Circle count: 2 Square count: 3 Triangle count: 2
      
      





map関数は、同様のアイデアに基づいて実装されています。その実装と実装自体の説明は省略します。 メタスキルをトレーニングするには、自分で実装することをお勧めします。



次は?



ミスについて少し。 一歩踏み込んで、同様のメッセージを確認してください。



エラー画面




このエラーのテキストは、非常に単純なコードを使用しても解析できません(エラーはファンクターのジェネリックパラメーターの誤った使用です)。 提示されたクラスよりもはるかに複雑なクラスを使用するとどうなるか想像してみてください。



エラーの本当の性質についてエレガントにまたはあまり言わないいくつかのアプローチがあります。

次回は、gcc-7.1のConcepts TSで書かれたものを希釈します。



要約すると、このアプローチは、 TypeErasureテクニックを使用しなければならないライブラリ、さまざまな特殊化されたテンプレートクラス、多態性のプリミティブエミュレーションなどで作業するのに非常に役立つと言えます。



そして、この機能をどのように追加/使用しますか? コメントを書いてください、読むのは面白いでしょう



上記のコードはこちらから入手できます



All Articles