Range-v3 practical examples

If you wish to learn functional programming in C++, there is no better place to start than the range-v3 library, the predecessor of C++20 Ranges. This page provides functional programming solutions to commonly encountered programming problems with range-v3.

Range-v3 vs C++20 Ranges

Only a very limited subset of the range-v3 library made it into C++20. views::zip_with was omitted from C++20 due to issues with proxy iterators and partial specializations (std::vector). Furthermore, some very useful views that can take care of cyclicity, grouping, striding, sliding etc. aren't part of C++20.

Range-v3

Try to understand the solutions published here in range-v3 before trying to find workarounds in C++20. This will help you develop a functional programming mindset.

You can find many more examples and online quizzes in the recently published book.

Getting started

Resistors in parallel

  1. Invert the values of resistance using views::transform. This will convert [20,10,15] to [0.05,0.1,0.0666667].
  2. Use accumulate to sum up the transformed resistance values.
  3. Invert the result of the accumulation to arrive at 4.61538 Ω.
#include <iostream>
#include <range/v3/all.hpp>

using namespace ranges;

int main(){      
    auto v = {20,10,15};
    auto r_inv = v | ranges::views::transform([](int x){return 1.0 / x;});
    // [0.05,0.1,0.0666667]
    auto val = 1.0 / accumulate(r_inv,0.0); // 1.0 / 0.216667 = 4.61538
    std::cout << val;
}

Binary to decimal conversion

  1. Reverse the initial binary representation using views::reverse arriving at [0,1,1,1]. This will allow us to start from the power 20.
  2. Generate the range [0,1,2,3] using views::iota.
  3. Use views::transform and the shift operator to convert [0,1,2,3] to [20, 21, 22, 23].
  4. Use views::inner_product to combine the reversed binary range [0,1,1,1] with the powers of two [1,2,4,8] and sum them up.
#include <iostream>
#include <range/v3/all.hpp>

using namespace ranges;

int main(){   
    auto const v = std::vector<uint8_t> {1,1,1,0};
    auto r_rev = v | views::reverse; // [0,1,1,1]
    auto r_int = views::iota(0, distance(v)); // [0,1,2,3]
    auto r_pow = r_int | views::transform([](int x){
                                return 1 << x;}); // [1,2,4,8]
    auto val = inner_product(r_rev,r_pow,0); // 0*1+1*2+1*4+1*8 = 14
    std::cout << val;
}

snake_case to CamelCase

  1. Split the string into separate words at underscore using views::split arriving at [[f,e,e,l],[t,h,e],[f,o,r,c,e]].
  2. Take the first letter of every word using views::take(1) and capitalize it using views::transform(...std::toupper...) , e.g. for [f,e,e,l] we get [F].
  3. Concatenate the first capitalized letter of a word with its tail using views::concat, e.g. for [F] and [e,e,l] we get [F,e,e,l].
  4. Flatten the range of words [[F,e,e,l],[T,h,e],[F,o,r,c,e]] into a single range [F,e,e,l,T,h,e,F,o,r,c,e] using views::join.
#include <iostream>
#include <range/v3/all.hpp>

using namespace ranges;

int main(){   
    auto const s = std::string{"feel_the_force"};
    auto words = s | views::split('_'); // [[f,e,e,l],[t,h,e],[f,o,r,c,e]]
    auto words_cap = words | views::transform([](auto w){
        auto head = w | views::take(1) 
                      | views::transform([](unsigned char c){return std::toupper(c);}); // e.g. [F]
        return views::concat(head, w | views::tail); // e.g. [F,e,e,l]
    }); // [[F,e,e,l],[T,h,e],[F,o,r,c,e]]
    auto s_camelcase = words_cap | views::join | to<std::string>(); // FeelTheForce
    std::cout << s_camelcase;
}

Would you like to get more practice?

Range-v3

Fibonacci sequence

Use views::generate with a mutable lambda. The keyword mutable allows us to update the initial pair p [0,1] in the body of the expression in the following way:

  • The first element of the new pair is the second element of the old pair, i.e. b0.
  • The second element of the new pair is the sum of the elements of the old pair, i.e. a0 + b0.
Note: To generate an element of the sequence we return only a single element. Returning a pair would result in duplicates in the output.

#include <iostream>
#include <range/v3/all.hpp>

using namespace ranges;

int main(){   
    auto rng = views::generate(
                [p = std::pair{0,1}] () mutable { 
                          auto [a0,b0] = p;
                          p = {b0, a0 + b0};
                          return a0;
                }); 
    auto fib10 = rng | views::take(10); // [0,1,1,2,3,5,8,13,21,34]
    std::cout << fib10;
}

Caesar cipher

Note: Single words only.

  1. Generate the cyclic alphabet using views::closed_iota followed by views::cycle getting [a,b,c...a,b,c...].
  2. Using views::drop drop the number of characters that corresponds to the shift getting [l,m,n...a,b,c...]
  3. Process every letter of the word "apple" using views::for_each. For example, for the letter e drop ('e'-'a') = (101 - 97) = 4 characters from the shifted alphabet. This means that [l,m,n...] becomes [p,q,r...]. To get the new letter e take the first letter of the transformed sequence, i.e. p.
#include <iostream>
#include <range/v3/all.hpp>

using namespace ranges;

int main(){   
    auto shift = 11;
    auto const s = std::string{"apple"};       

    auto alphabet = views::closed_iota('a','z') | views::cycle; // [a,b,c...a,b,c...]
    auto shifted_alphabet = alphabet | views::drop(shift); // [l,m,n...a,b,c...]

    auto encrypted = s | views::for_each([shifted_alphabet](char letter){
        return shifted_alphabet | views::drop(letter - 'a') | views::take(1); 
    }); // [l,a,a,w,p]
    auto s_encrypted = encrypted | to<std::string>(); // laawp
    std::cout << s_encrypted;
}

Triangular sequence

  1. Generate the range [1,2,3...] using views::iota.
  2. Modify the generated range using views::transform(...n*(n+1)/2...) getting [1,3,6..].
  3. Take the desired number of elements using views::take.
#include <iostream>
#include <range/v3/all.hpp>

using namespace ranges;

int main(){   
    auto r_int = views::iota(1); // [1,2,3,4,5,6,7,8...]
    auto r_triseq = r_int | views::transform([](int n){return n*(n+1)/2;}); // [1,3,6,10,15,21,28...]
    auto r_first5 = r_triseq | views::take(5); // [1,3,6,10,15]
    std::cout << r_first5;
}

Note: Range-v3 has a convenience view to accomplish the same task, namely views::partial_sum.

#include <iostream>
#include <range/v3/all.hpp>

using namespace ranges;

int main(){   
    auto r_int = views::iota(1); // [1,2,3,4,5,6,7,8...]
    auto r_triseq = r_int | views::partial_sum(std::plus{}); // [1,3,6,10,15,21,28...]
    std::cout << (r_triseq | views::take(5)); // [1,3,6,10,15]
}

Aronson's sequence

Aronson's sequence is a self-describing integer sequence defined by the sentence "T is the first, fourth, eleventh, sixteenth, twenty-fourth, twenty-ninth, thirty-third... letter in this sentence." Generate the sequence ignoring spaces and punctuation.

#include <iostream>
#include <range/v3/all.hpp>

using namespace ranges;

int main(){       
    auto s = std::string{"T is the first, fourth, eleventh, sixteenth, "
                         "twenty-fourth, twenty-ninth, thirty-third..."};
    auto r_letters_only = s | views::filter([](unsigned char c){
                                              return std::isalpha(c);});
    // [T,i,s,t,h,e,f,i,r,s,t,f,o,u,r,t,h...]
    auto r_lowercase = r_letters_only | views::transform([](unsigned char c){
                                                        return std::tolower(c);});
    // [t,i,s,t,h,e,f,i,r,s,t,f,o,u,r,t,h...]
    auto r_pairs = views::zip(r_lowercase, views::iota(1));
    // (t:1)(i:2)(s:3)(t:4)...
    auto r_only_tpairs = r_pairs | views::filter([](auto && r){return r.first == 't';});
    // (t:1)(t:4)...
    auto r_aronson = r_only_tpairs | views::values;
    // [1,4,11,16,24,29,33,35,39,45,47,51,56,58,62,64]
    std::cout << r_aronson;  
}

Shannon's entropy

Shannon's entropy is a measure of system's disorganisation. If all the balls in the bag below are red, the probability vector would be [1,0,0,0], hence the resulting entropy would be 0. We can interpret this as that there would be no surpise in the outcome. The example below demonstrates the entropy calculation for 4 red, 2 gray, one black and one white ball. To calculate the entropy:

  1. Transform the probability vector using views::transform(...-p*std::log2(p)...).
  2. Sum up the elements of the transformed probability range using accumulate.
#include <iostream>
#include <range/v3/all.hpp>

using namespace ranges;

int main(){   
    auto const v = std::vector{0.5,0.25,0.125,0.125};
    auto r_p_logp =  v | views::transform([](auto p){return -p*std::log2(p);});
    auto val = accumulate(r_p_logp, 0.0); // 1.75
    std::cout << val;
}

Dot product

Note: This is a rough equivalent of ranges::inner_product.

To get the dot product:

  1. Multiply the corresponding elements of the input vectors with each other using views::zip_with(std::multiplies{}...), e.g. here [1.5*1, 2.5*2, 3.5*3].
  2. Sum up the values of the resulting range using accumulate, e.g. here 1.5 + 5 + 10.5.
#include <iostream>
#include <range/v3/all.hpp>

using namespace ranges;

auto dotProduct = [](auto && r1, auto && r2){
                               return accumulate(
                                      views::zip_with(std::multiplies{},r1,r2), 0.0);};
int main(){   
    auto const v1 = std::vector{1.5,2.5,3.5};
    auto const v2 = std::vector{1,2,3};
    std::cout << dotProduct(v1,v2); // 1.5*1 + 2.5*2 + 3.5*3 = 17
}

Permutations with repetition

#include <iostream>
#include <range/v3/all.hpp>

using namespace ranges;

int main(){   
    auto const v = std::vector{'a','b','c'}; 
    auto rng = views::cartesian_product(v,v,v);
    for(auto const & [x,y,z] : rng)
        std::cout << x << ' ' << y <<  ' ' << z << '\n';
}
// 27 permutations with repetition
// a a a
// a a b
// a a c
// a b a
// a b b
// ...

Permutations without repetition

Note: Sort the sequence first.

#include <iostream>
#include <range/v3/all.hpp>

using namespace ranges;

int main(){   
    auto s = std::string{"cab"};
    auto s2 = s;
    sort(s2);
    do {
        std::cout << s2 << '\n';
    } while (next_permutation(s2));
}
// abc
// acb
// bac
// bca
// cab
// cba

Estimate area under a function

Estimate the area under the function x3 on the interval (0,2) using the midpoint Riemann sums. The figure below depicts approximately half of the area of interest.

#include <iostream>
#include <range/v3/all.hpp>

using namespace ranges;

int main(){   
    auto steps = 5;
    auto a = 0.0;
    auto b = 2.0;
    auto dx = (b-a)/steps; // 0.4
    auto f_cubic = [](double x){return x*x*x;};
    
    auto r_int = views::iota(0, steps); // [0,1,2,3,4]
    auto r_pos = r_int | views::transform([dx](int i){return dx*(0.5 + i);}); 
    // [0.2,0.6,1,1.4,1.8]
    auto r_cubic = r_pos | views::transform(f_cubic); 
    // [0.008, 0.216, 1, 2.744, 5.832]
    auto area = dx*accumulate(r_cubic, 0.0); 
    // 0.4*(0.008 + 0.216 + 1 + 2.744 + 5.832) = 3.92 
    std::cout << area;
}

Tagged: Range-v3