C++20: Pythonic with the Ranges Library

Contents[Show]

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.

 TimelineCpp20

You maybe know it. I'm also a Python trainer since 2004. Python has awesome 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" (Pythons 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 which 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 stand 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. The Python code is often shorter than the 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-ages 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.

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 the 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. 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 an empty std::vector<int>.

I cheat a little in my range implementation. I use the function ranges::views::stride that 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>.

In the else case (line 2), I use a little trick. I create the numbers [end++, begin++[, take them until the boundary condition is meet, 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 just apply the callable func to each element of the sequence 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 in line (4). Line (5) counts, how many lines in the file "/etc/services" are comments. Comments are lines which start with the '#' character.

If you ignore the different ways to implement lambdas in Python and in 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: Meeting C++, Matt Braun, Roman Postanciuc, Venkata Ramesh Gudpati, Tobias Zindl, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Richard Ohnemus, Frank Grimm, Sakib, Broeserl, António Pina, Markus Falkner, Darshan Mody, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, and Jozo Leko.

 

Thanks in particular to:   crp4

 

   

Get your e-book at Leanpub:

The C++ Standard Library

 

Concurrency With Modern C++

 

Get Both as one Bundle

cover   ConcurrencyCoverFrame   bundle
With C++11, C++14, and C++17 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages. I also included more than 120 source files.  

C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.

I'll give you a detailed insight in the current and the upcoming concurrency in C++. This insight includes the theory and a lot of practice with more than 140 source files.

 

Get my books "The C++ Standard Library" (including C++17) and "Concurrency with Modern C++" in a bundle.

In sum, you get more than 700 pages full of modern C++ and more than 260 source files presenting concurrency in practice.

 

Get your interactive course

 

Modern C++ Concurrency in Practice

C++ Standard Library including C++14 & C++17

educative CLibrary

Based on my book "Concurrency with Modern C++" educative.io created an interactive course.

What's Inside?

  • 140 lessons
  • 110 code playgrounds => Runs in the browser
  • 78 code snippets
  • 55 illustrations

Based on my book "The C++ Standard Library" educative.io created an interactive course.

What's Inside?

  • 149 lessons
  • 111 code playgrounds => Runs in the browser
  • 164 code snippets
  • 25 illustrations

My Newest E-Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 4912

All 4183719

Currently are 239 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments