C++20:Pythonic with Ranges Library

今日、私は実験を始めます。範囲ライブラリを使用して、C++ の Python で最愛の関数を実装したいと考えています。どうなるか興味津々です。

あなたは多分それを知っています。私は 2004 年から Python のトレーナーでもあります。Python には素晴らしい機能があり、多くの場合、Python は私にとってプログラミング言語がどれほど快適であるかのしきい値です。今日は、Python 関数の range と filter を実装したいと思います。

  • range は、「整数の等差数列を含む」リストを作成します (Python の組み込みヘルプ)。
  • filter は述語をシーケンスに適用し、述語が true を返す要素を返します。

シーケンスは Python の用語で、リスト ([1, 2, 3])、タプル ((1, 2, 3))、文字列 ("123") などの反復可能なものを表します。リストの代わりに、C++ で std::vector を使用します。関数フィルターは、Python の関数型スタイルを表します。

範囲関数について説明する前に、いくつかコメントしておく必要があります。

<オール>
  • 私の例では、Eric Niebler の range-v3 ライブラリを使用しています。これは、C++20 レンジの基礎です。以前の投稿 C++20:The Ranges Library で、ranges-v3 を C++20 構文に変換する方法を示しました。
  • Python コードは、多くの場合、2 つの理由から C++ コードよりも短くなります。まず、Python のリストを変数に保存しません。次に、結果を表示しません。
  • プログラミング言語をめぐる宗教戦争は好きではありません。中世はとうの昔に過ぎ去りました。これらのコメントには反応しません。
  • 範囲機能から始めましょう。範囲関数は、整数を作成するためのビルディング ブロックの一種です。

    範囲

    次の例では、最初にコメント アウトされた python 式を示し、次に対応する C++ 呼び出しを示します。

    // range.cpp
    
    #include <iostream>
    #include <range/v3/all.hpp>
    #include <vector>
    
    std::vector<int> range(int begin, int end, int stepsize = 1) {
     std::vector<int> result{};
     if (begin < end) { // (5)
     auto boundary = [end](int i){ return i < end; };
     for (int i: ranges::views::iota(begin) | ranges::views::stride(stepsize) 
     | ranges::views::take_while(boundary)) {
     result.push_back(i);
     }
     }
     else { // (6)
     begin++;
     end++;
     stepsize *= -1;
     auto boundary = [begin](int i){ return i < begin; };
     for (int i: ranges::views::iota(end) | ranges::views::take_while(boundary) 
     | ranges::views::reverse 
     | ranges::views::stride(stepsize)) {
     result.push_back(i);
     }
     }
     return result;
    }
     
    int main() {
     
     std::cout << std::endl;
    
     // range(1, 50) // (1)
     auto res = range(1, 50);
     for (auto i: res) std::cout << i << " ";
     
     std::cout << "\n\n";
     
     // range(1, 50, 5) // (2)
     res = range(1, 50, 5);
     for (auto i: res) std::cout << i << " ";
     
     std::cout << "\n\n";
     
     // range(50, 10, -1) // (3)
     res = range(50, 10, -1);
     for (auto i: res) std::cout << i << " ";
     
     std::cout << "\n\n";
     
     // range(50, 10, -5) // (4)
     res = range(50, 10, -5);
     for (auto i: res) std::cout << i << " ";
     
     std::cout << "\n\n";
     
    }
    

    行 (1) から (4) の呼び出しは、出力を見ると非常に読みやすいはずです。

    範囲呼び出しの最初の 2 つの引数は、作成された整数の先頭と末尾を表します。始まりは含まれますが、終わりは含まれません。 3 番目のパラメーターとしてのステップ サイズは、デフォルトでは 1 です。間隔 [begin, end[] が減少している場合、ステップ サイズは負の値にする必要があります。そうでない場合は、空のリストまたは空の std::vector を取得します。

    範囲の実装で少しごまかしています。 C++20 の一部ではない関数 range::views::stride を使用します。 stride(n) は、指定された範囲の n 番目の要素を返します。 C++20 に基づく洗練された実装をご存知でしたら、お知らせください。

    行 (1) の range 関数の if 条件 (begin にプッシュします。

    else の場合 (2 行目) では、ちょっとしたトリックを使用します。数値 [end++, begin++[] を作成し、境界条件が満たされるまで取得し、それらを反転させ (ranges::views::reverse)、各 n 番目の要素を取得します。

    私の例では、フィルターとマップ (次の投稿) の熱心なバージョンを実装しています。 Python 3 ではフィルターとマップが遅延します。この場合、フィルターとマップはジェネレーターを返します。 Python 2 の熱心な動作を取得するには、Python 3 でフィルターとマップの呼び出しをリストで囲みます。

    filter(lambda i: (i % 2) == 1 , range(1, 10)) # Python 2 
    
    list(filter(lambda i: (i % 2) == 1, range(1, 10))) # Python 3
    

    両方の呼び出しで同じリストが生成されます:[1, 3, 5, 7, 9].

    マップ関数などの実装が簡単なので、関数フィルターを続けます。

    フィルター

    // filter.cpp
    
    #include "range.hpp" // (1)
    
    #include <fstream>
    #include <iostream>
    #include <range/v3/all.hpp>
    #include <sstream> #include <string> #include <vector> #include <utility> template <typename Func, typename Seq> // (2) auto filter(Func func, Seq seq) { typedef typename Seq::value_type value_type; std::vector<value_type> result{}; for (auto i : seq | ranges::views::filter(func)) result.push_back(i); return result; } int main() { std::cout << std::endl; // filter(lambda i: (i % 3) == 0 , range(20, 50)) // (3) auto res = filter([](int i){ return (i % 3) == 0; }, range(20, 50) ); for (auto v: res) std::cout << v << " "; // (4) // filter(lambda word: word[0].isupper(), ["Only", "for", "testing", "purpose"]) std::vector<std::string> myStrings{"Only", "for", "testing", "purpose"}; auto res2 = filter([](const std::string& s){ return static_cast<bool>(std::isupper(s[0])); }, myStrings); std::cout << "\n\n"; for (auto word: res2) std::cout << word << std::endl; std::cout << std::endl; // (5) // len(filter(lambda line: line[0] == "#", open("/etc/services").readlines())) std::ifstream file("/etc/services", std::ios::in); std::vector lines;
    std::string line;
    while(std::getline(file, line)){
    lines.push_back(line);
    } std::vector<std::string> commentLines = filter([](const std::string& s){ return s[0] == '#'; }, lines); std::cout << "Comment lines: " << commentLines.size() << "\n\n"; }

    プログラムを説明する前に、出力をお見せしましょう。

    今回は先ほどの範囲実装を含めます。フィルター関数 (2 行目) は読みやすいはずです。呼び出し可能な func をシーケンスの各要素に適用し、要素を std::vector で具体化するだけです。行 (3) は、(i % 3) ==0 を保持する 20 から 50 までのすべての数値 i を作成します。大文字で始まる文字列のみがフィルター インライン (4) を通過できます。行 (5) は、ファイル「/etc/services」内のコメント行数です。コメントは「#」文字で始まる行です。

    Python と C++ でラムダを実装するさまざまな方法を無視すると、フィルター呼び出しは非常に似ています。

    次は?

    map は filter よりも実装が複雑でした。まず、map は入力シーケンスの型を変更する場合があります。次に、マップの実装が GCC バグ レポートをトリガーしました。その後、関数 map と filter を関数に結合すると、 ... が得られます。詳細は次回の投稿でお読みください。