Ranges Improvements with C++23

Contents[Show]

Thanks to C++23, constructing containers will become more convenient. Additionally, the ranges library got more new views.

 

Cpp23

Ranges

C++23 is not such a significant standard as C++11 or C++20. It's more in the tradition of C++17. This is mainly due to COVID-19 because the annual four face-to-face meetings went online. Essentially, the ranges library is the exception to this rule. The ranges will get a few crucial additions.

If you know more details about what you can expect about C++23 (before I write about it), study cppreference.com/compiler_support. Even better, read the excellent paper from Steve Downey (C++23 Status Report).

 

Rainer D 6 P2 540x540Modernes C++ Mentoring

Be part of my mentoring programs:

 

 

 

 

Do you want to stay informed about my mentoring programs: Subscribe via E-Mail.

Constructing Containers

Constructing a container from a range was a complicated job. The following function range simulates python2's range function. Python2's range function is eager, and so is its range pendant: Additionally, Pythons range function returns a list, but mine a std::vector.

 

// 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 pretty easy to read when looking at the output.

range

The first two arguments of the range call stand for the beginning and end of the created integers. The begin is included but not the end. The step size as the third parameter is, per default, 1. The step size should be negative when the interval [begin, end] decreases. 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, which is not part of C++20. stride(n) returns the n-th element of the given range. I assume that std::views::stride becomes part of C++23, but I'm not sure. Consequentially, I used the ranges v3 implementation in my example but not the C++20 implementation of the ranges library.

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.

Now, let's assume that std::views::stride is part of C++23. Thanks to std::ranges::to, it's pretty easy to construct a Container. Here is the C++23-based implementation of the previous range function.

 

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; };
        result = std::ranges::views::iota(begin) | std::views::stride(stepsize) 
                                                 | std::views::take_while(boundary) 
                                                 | std::ranges::to<std::vector>();
    }
    else {                                                
        begin++;
        end++;
        stepsize *= -1;
        auto boundary = [begin](int i){ return i < begin; };
        result = std::ranges::views::iota(end) | std::views::take_while(boundary) 
                                               | std::views::reverse 
                                               | std::views::stride(stepsize) 
                                               | std::ranges::to<std::vector>();
    }
    return result;
} 

 

Essentially, I replaced the push_back operation on the std::vector with the new call std::ranges::to<std::vector>, and got rid of two lines of code. So far, no compiler supports this new convenient function to create a container. I created the new range function based on my interpretation of the specification. If there is an error included, I will fix it.

Existing Algorithms in C++20

Before I show you the new views in C++23, here are the already existing ones in C++20:

 viewNew

New Views in C++23

Now, I want to present to you the new views. If possible, I will provide you with a short code example.

  • std::ranges::views::zip_transform, and std::views::zip_transform

Creates a view that consists of tuples by applying a transformation function.

Here is an excellent example from cppreferene.com/zip_transform_view:

#include <list>
#include <array>
#include <ranges>
#include <vector>
#include <iostream>
 
void print(auto const rem, auto const& r) {
    for (std::cout << rem; auto const& e : r)
        std::cout << e << ' ';
    std::cout << '\n';
}
 
int main() {
    auto v1 = std::vector<float>{1, 2, 3};
    auto v2 = std::list<short>{1, 2, 3, 4};
    auto v3 = std::to_array({1, 2, 3, 4, 5});
 
    auto add = [](auto a, auto b, auto c) { return a + b + c; };
 
    auto sum = std::views::zip_transform(add, v1, v2, v3);
 
    print("v1:  ", v1);    // 1 2 3
    print("v2:  ", v2);    // 1 2 3 4
    print("v3:  ", v3);    // 1 2 3 4 5
    print("sum: ", sum);   // 3 6 9
}

 

I added the output directly into the source code.

  • std::ranges::adjacent_view, std::views::adjacent_view, std::ranges::adjacent_transform_view, and std::views::adjacent_transform

Creates a view that consists of tuples of references to adjacent elements. Additionally, You can apply a transformation function.

These examples are directly from the proposal P2321R2:

 

vector v = {1, 2, 3, 4};

for (auto i : v | views::adjacent<2>) {
  cout << '(' << i.first << ', ' << i.second << ") "; // prints: (1, 2) (2, 3) (3, 4)
}

for (auto i : v | views::adjacent_transform<2>(std::multiplies())) {
  cout << i << ' ';  // prints: 2 6 12
}

 

  • std::ranges::join_with, and std::views::join_with

Creates a view by flattening the input range. Puts a delimiter between elements.

cppreference.com/join_with_view provides a nice example in which a space is the delimiter element.

#include <iostream>
#include <ranges>
#include <vector>
#include <string_view>
 
int main() {
    using namespace std::literals;
 
    std::vector v{"This"sv, "is"sv, "a"sv, "test."sv};
    auto joined = v | std::views::join_with(' ');
 
    for (auto c : joined) std::cout << c;
    std::cout << '\n';
}

 

  • std::views::chunk, and std::views::chunk_by

Creates a view by dividing a range R into non-overlapping N-sized chunks. Additionally, you can apply a predicate.

The code snippets are from proposal P2442R1 and proposal P2443R1.

 

std::vector v = {1, 2, 3, 4, 5};
fmt::print("{}\n", v | std::views::chunk(2));   // [[1, 2], [3, 4], [5]]
fmt::print("{}\n", v | std::views::slide(2));   // [[1, 2], [2, 3], [3, 4], [4, 5]]


std::vector v = {1, 2, 2, 3, 0, 4, 5, 2};
fmt::print("{}\n", v | std::views::chunk_by(ranges::less_equal{}));   // [[1, 2, 2, 3], [0, 4, 5], [2]]

 

Both code snippets use the prototype library fmt for the format library in C++20. fmt has a convenience function fmt::print that may become part of C++23 as std::print.

  • std::views::slide

Creates a view of N-tuples by taking a view and a number N.

The example is also from proposal P2443R1.

vector v = {1, 2, 3, 4};

for (auto i : v | views::slide(2)) {
  cout << '[' << i[0] << ', ' << i[1] << "] "; // prints: [1, 2] [2, 3] [3, 4]
}

What's next?

Last week, I made a poll and asked: "Which mentoring program should I implement next?" Honestly, this result surprised me a lot. I taught Design Patterns from 2004 to 2008 and assumed that you already knew them, and C++20 or Clean Code with C++ would win the poll. Consequentially, I changed my plan for my upcoming posts. My next big topic will be "Design Pattern and Architectural Pattern in C++". When I finish this big topic, I will return to C++20 and C++23.

 

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, Animus24, 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, Matthieu Bolt, 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, and Rob North.

 

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

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

My special thanks to PVS-Studio PVC Logo

 

My special thanks to Tipi.build tipi.build logo

 

My special thanks to Take Up Code TakeUpCode 450 60

 

Seminars

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

Bookable (Online)

German

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++

New

  • Clean Code with Modern C++
  • C++20

Contact Me

Modernes C++,

RainerGrimmDunkelBlauSmall

Tags: Python, Ranges

Comments   

0 #1 Farid 2022-06-20 13:31
Both versions of the `range` function will fail to compile for the same reason: when refactoring before publish, the same mistake has been repeated. `boundary` is supposed to mark the indexend index, but is initialized to the lambda. The lambda call has been ommited; hence the `result` is going to be empty. The `result` is neither in capture nor parameter list of the lambda; hence a hard error is thrown at compile-time. You probably should've returned a filtered `iota`(no `vector` involved), so there is not memory overhead and leave the conversion to the user code.
Quote

Stay Informed about my Mentoring

 

Mentoring

English 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

Course: The All-in-One Guide to C++20

Course: Master Software Design Patterns and Architecture in C++

Subscribe to the newsletter (+ pdf bundle)

All tags

Blog archive

Source Code

Visitors

Today 1798

Yesterday 4344

Week 38676

Month 18922

All 12097131

Currently are 149 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments