C++20:Pythons range 関数、2 番目

前回の投稿 C++20:Pythonic with the Ranges Library で、愛されている Python 関数 range と filter を C++ で実装する実験を開始しました。前回の投稿に 2 つの非常に興味深いコメントがあったため、関数の範囲を再検討します。

確かに、範囲ライブラリに慣れるまでにはかなりの時間がかかりましたが、その努力は報われました。理由がわかります。

前回の投稿で、範囲の実装について非常に興味深いコメントをいくつか受け取りました。したがって、もう一度訪問する必要があります。

範囲

簡単なメモとして。呼び出し range(begin, end, step) は、Python 2 で開始から終了までのすべての整数のリストをステップサイズのステップで生成します。 begin は包括的で end は排他的です。 step はデフォルトで 1 です。

オーバーエンジニアリング

ドイツの読者の 1 人が指摘したように、最後の範囲の実装は過剰に設計されていました。次のコード スニペットは、過度に設計されたバージョンと改善されたバージョンを示しています。

std::vector<int> range(int begin, int end, int stepsize = 1) {
 std::vector<int> result{};
 if (begin < end) { 
 auto boundary = [end](int i){ return i < end; }; 
 for (int i: ranges::views::iota(begin) // (2)
 | ranges::views::stride(stepsize) 
 | ranges::views::take_while(boundary)) { // (1)
 result.push_back(i);
 }
 }
 else { 
 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;
} 

std::vector<int> range(int begin, int end, int stepsize = 1) {
 std::vector<int> result{};
 if (begin < end) {
 for (int i: ranges::views::iota(begin, end) // (3)
 | ranges::views::stride(stepsize)) {
 result.push_back(i);
 }
 }
 else {
 begin++;
 end++;
 stepsize *= -1;
 for (int i: ranges::views::iota(end, begin) 
 | ranges::views::reverse 
 | ranges::views::stride(stepsize)) {
 result.push_back(i);
 }
 }
 return result;
}

最初の実装で境界条件 (1 行目) を削除し、無限数ジェネレーターの range::views::iota(begin) (2 行目) を有限数ジェネレーターの range::view::iota(begin, end) に変更しました。 (3 行目)。結果として、else ブランチでも同じことを行いました。

範囲から xrange へ

提示された範囲関数は熱心です。 std::vector を生成します。 Aleksei Guzev は、Python 2 にも Python 3 の range 関数に対応する遅延 xrange 関数があることを思い出させてくれました。彼は正しい。これで、Range ライブラリに十分慣れて、関数の概念を C++ に適用できるようになりました。熱心で怠け者という用語に困惑している場合は、以前の投稿 C++20:Ranges ライブラリを使用した関数型パターン をお読みください。

次の例は、範囲の遅延バリアントを示しています。これは、結果的に xrange と呼びました。

// xrange.hpp

#include <range/v3/all.hpp>

template <long long Begin, long long End> // (3)
auto xrange(int stepsize = 1) {
 if constexpr (Begin < End) { // (2)
 return ranges::views::iota(Begin, End) // (1)
 | ranges::views::stride(stepsize); 
 }
 else {
 long long end = End + 1; // (4)
 long long begin = Begin + 1; // (4)
 stepsize *= -1; 
 return ranges::views::iota(end, begin) // (1)
 | ranges::views::reverse 
 | ranges::views::stride(stepsize);
 }
}

この遅延 xrange 関数の実装は、以前の熱心な範囲関数よりもはるかに複雑です。しかし、追加された複雑さは報われます。次の番号は、ソース コード スニペットの番号に対応しています。

<オール>
  • xrange 関数は、std::vector ではなくビューの構成を返します。仕事を楽にするために、auto を使用してコンパイラに戻り値の型を推測させます。結構ですが、戻り型が最初の課題を引き起こしました。 if および else 分岐ダイバーの戻り値の型。戻り値の型が異なる関数は有効な C++ ではありません。
  • この問題を克服するために、C++17 の機能である constexpr if を使用しました。 constexpr 条件付きコンパイルを許可する場合。式 if constexpr (Begin
  • Begin と End は非型テンプレート パラメータになり、constexpr if (2 行目) 式で使用できるようになりました。大きな数を処理するために、long long 型の非型テンプレート パラメーターを使用しました。あなたはいくつかの文で読んでください、なぜ.
  • Begin や End などの定数式は変更できません。結果として、変数 end と begin を導入して、ranges::views::iota 呼び出しの境界を適応させました。
  • 試してみましょう。

    // range.cpp
    
    #include "xrange.hpp"
    
    #include <iostream>
    #include <range/v3/all.hpp>
    #include <vector>
    
     
    int main() {
     
     std::cout << std::endl;
    
     auto res = xrange<1, 10>(); // (1)
     for (auto i: res) std::cout << i << " ";
     
     std::cout << "\n\n";
     
     res = xrange<1, 50>(5); // (2)
     for (auto i: res) std::cout << i << " ";
     
     std::cout << "\n\n";
     
     auto res2 = xrange<20, 10>(-1); // (3)
     for (auto i: res2) std::cout << i << " ";
     
     std::cout << "\n\n";
     
     res2 = xrange<50, 10>(-5); // (4)
     for (auto i: res2) std::cout << i << " ";
     
     std::cout << "\n\n";
     
     res = xrange<1, 1'000'000'000'000'000'000>(); // (5)
     // for (auto i: res) std::cout << i << " "; // (6)
     
     
     // (7)
     for (auto i: res | ranges::views::take(10)) std::cout << i << " ";
     
     std::cout << "\n\n";
     
     // (8)
     for (auto i: res | ranges::views::drop_while([](int i){ return i < 1'000'000; })
     | ranges::views::take_while([](int i){ return i < 1'000'010; })) {
     std::cout << i << " ";
     }
     
     std::cout << "\n\n";
     
    }
    

    行 (1) ~ (4) は、xrange 関数が前の range 関数として機能することを示しています。唯一の違いは、関数の引数がテンプレートの引数になることです。すべての数値を 1000 京 (6 行目) まで取得したい場合は、プログラムを強制終了する必要があります。

    数値 (1'000'000'000'000'000'000) (5 行目) にティックを使用することは、C++14 以降で有効であり、大きな数値を読みやすくします。私はそれほど熱心ではなく、怠け者であるべきです。 10 個の数字 (7 行目) または 1'000'000 から 1'000'010 の間の数字 (8 行目) のみを要求すると、プログラムは魅力的に機能します。要求された数字のみが生成されます。

    次は?

    前回の投稿 C++20:Pythonic with the Ranges Library で既に約束したように、次回の投稿では Python の map 関数を紹介します。 map を使用すると、関数をシーケンスに適用できます。便宜上、マップとフィルター関数を 1 つの関数に結合します。