C++20: Pythonic with the Ranges Library

Today, I start an experiment. I want to implement beloved functions in Python in C++ using the ranges library. I’m curious about how it goes.

You maybe know it. I’m also a Python trainer since 2004. Python has excellent functions, and often Python is, for me, the threshold of how comfortable a programming language could be. Today, I want to implement the Python functions range and filter.

  • range creates a list “containing an arithmetic progression of integers” (Python built-in help).
  • filter applies a predicate to a sequence and returns those elements for which the predicate returns true.

A sequence is a term in Python that stands for something iterable such as a list ([1, 2, 3]), a tuple ((1, 2, 3)), or a string (“123”). Instead of a list, I use a std::vector in C++. The functions filter stands for the functional style in Python.

Before I start with the range function, I have to make a few remarks.

  1. I use in my examples the range-v3 library from Eric Niebler, which is the basis for the C++20 ranges. I showed in my previous post, C++20: The Ranges Library, how to translate the ranges-v3 to the C++20 syntax.
  2. Python code is often shorter than C++ Code for two reasons. First, I don’t store the Python lists in a variable, and second, I don’t display the result.
  3. I don’t like religious wars about programming languages. The middle-aged are long gone. I will not react to these comments.

Let’s start with the range function. The range function is a kind of building block for creating integers.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

    range

    In the following examples, I first show the Python expression commented out and then the corresponding C++ call.

    // 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";
        
    }
    

    The calls in lines (1) – (4) should be quite easy to read when you look at the output.

    range

    The first two arguments of the range call stand for the beginning and end of the created integers. The beginning is included but not the end. Step size as the third parameter is per default 1. When the interval [begin, end[ is decreasing, the step size should be negative. If not, you get an empty list or std::vector<int>.

    I cheat a little in my range implementation. I use the function ranges::views::stride which is not part of C++20. stride(n) returns the n-th element of the given range. If you know an elegant implementation based on C++20, please let me know.

    The if condition (begin < end) of the range function in line (1) should be quite easy to read. Create all numbers starting with begin (ranges::views::iota(begin)), take each n-th element (ranges::views::stride(stepsize), and do it as long as the boundary condition holds (ranges::views::take_while(boundary). Finally, push the integers on the std::vector<int>.

    I use a little trick in the other case (line 2). I create the numbers [end++, begin++[, take them until the boundary condition is met, reverse them (ranges::views::reverse), and take each n-th element.

    I implement the eager version for filter and map (next post) in my examples. With Python 3 filter and map are lazy. filter and map return, in this case, generators. To get the eager behavior of Python 2, put a list around the filter and map calls in 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
    

    Both calls produce the same list: [1, 3, 5, 7, 9].

    I continue with the function filter because it is easier to implement, such as the map function.

    filter

    // 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";
                                              
    }
    

    Before I explain the program, let me show you the output.

    filter

    This time, I include the range implementation from before. The filter function (line 2) should be easy to read. I apply the callable func to each sequence element and materialize the elements in the std::vector. Line (3) creates all numbers i from 20 to 50 for which hold (i % 3) == 0. Only the strings that start with an uppercase letter can pass the filter inline (4). Line (5) counts how many lines in the file “/etc/services” are comments. Comments are lines that start with the ‘#’ character.

    If you ignore the different ways to implement lambdas in Python and C++, the filter calls are quite similar.

    What’s next?

    map was way more complicated to implement than filter. First, map may change the type of the input sequence. Second, my implementation of map triggered a GCC bug report. Afterward, I combine the functions map and filter in a function and I get … . Read the details in my next post.

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, moon, Philipp Lenk, Hobsbawm, and Charles-Jianye Chen.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Seminars

    I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

    Standard Seminars (English/German)

    Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

    • C++ – The Core Language
    • C++ – The Standard Library
    • C++ – Compact
    • C++11 and C++14
    • Concurrency with Modern C++
    • Design Pattern and Architectural Pattern with C++
    • Embedded Programming with Modern C++
    • Generic Programming (Templates) with C++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *