Back to TILs

C++ Ranges — Part 1

Date: 2022-12-13Last modified: 2023-06-27
Photo by Ning Shi on Unsplash

Introduction

C++ ranges and views are both library features introduced in C++20 that provide a way to work with sequences of values, such as arrays or containers, in a more expressive and composable way. Here are some of the key differences between ranges and views:

In summary, ranges provide a uniform way to access and manipulate sequences of elements, while views provide a composable and lazy way to create subsets or transformations of ranges. Both ranges and views can be useful in different contexts, depending on the requirements of the program or algorithm being implemented.

Views

Range Algorithm

Example of legacy algorithm

The operations are made by using iterators.

void exampleLegacyAlgorithm()
{
  int collection[]{ 2, 6, 8, 40, 64, 70 };
  if( std::all_of( std::begin( collection ), std::end( collection ), []( int i ) { return i % 2 == 0; } ) ) {
    std::cout << "(std::all_of) : All numbers in collection are even" << std::endl;
  }
  else {
    std::cout << "(std::all_of) : Not all numbers in collection are even" << std::endl;
  }
}

Example of range algorithm

The operations are performed directly on collection.

void exampleRangeAlgorithm()
{
  std::vector<int> collection{ 2, 6, 8, 40, 64, 70 };
  auto             result = std::ranges::all_of( collection, []( int i ) { return i % 2 == 0; } );
  if( result ) {
    std::cout << "All numbers in collection are even" << std::endl;
  }
  else {
    std::cout << "Not all numbers in collection are even" << std::endl;
  }
}

Struct to exemplify a more complex data type

struct Student {
  friend std::ostream &operator<<( std::ostream &out, const Student &s )
  {
    out << "Student [ name: " << s.m_name << ", age: " << s.m_age << "]";
    return out;
  }

  auto         operator<=>( const Student &s ) const = default;
  std::string  m_name;
  unsigned int m_age;
};

Specialized printing data type

template <>
struct fmt::formatter<Student> {
  // parse is a no-op as we do not need any format specifiers
  constexpr auto parse( format_parse_context &ctx )
  {
    return ctx.begin();
  }

  // format function for Student
  template <typename FormatContext>
  auto format( const Student &s, FormatContext &ctx )
  {
    return format_to( ctx.out(), "{{ name: '{}', age: {} }}", s.m_name, s.m_age );
  }
};

Utility to print all collection elements

  auto printContainer = []( const auto label, const auto &container ) {
    fmt::print( "{:30}: ", label );
    for( const auto &i : container ) {
      fmt::print( "{} ", i );
    }
    fmt::print( "\n" );
  };

Utility to print all view elements

  auto printView = []( const auto label, auto view ) {
    fmt::print( "{:30}: ", label );
    for( auto i : view ) { // Computation happens here.
      fmt::print( "{} ", i );
    }
    fmt::print( "\n" );
  };

Utilities to be used as filter

  auto even   = []( int i ) { return i % 2 == 0; };
  auto odd    = []( int i ) { return i % 2 == 1; };
  auto square = []( int i ) { return i * i; };

Range and view examples of use

  exampleLegacyAlgorithm();
  exampleRangeAlgorithm();

Loop detect array length

  int ints[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
  for( [[maybe_unused]] const auto i : ints ) {
  }
  printContainer( "ints", ints );
  // (4,10] = 4 5 6 7 8 9
  for( [[maybe_unused]] int i : std::ranges::iota_view{ 4, 10 } ) {
  }
  printContainer( "iota_view{4,10}", std::ranges::iota_view{ 4, 10 } );
  // (5,10] = 5 6 7 8 9
  for( [[maybe_unused]] int i : std::views::iota( 5, 10 ) ) {
  }
  printContainer( "views::iota(5, 10)", std::views::iota( 5, 10 ) );

Both std::ranges::transform_view and std::views::transform provide a way to lazily apply a transformation to elements of a range, and they have similar syntax and functionality. However, there are some differences in their use cases and performance characteristics.

std::ranges::transform_view is part of the Ranges library introduced in C++20 and is designed to work with ranges that support the Ranges API. It provides a range view that applies a transformation function to each element of a range, producing a new range with the transformed elements. It can be used in conjunction with other Ranges library components to perform complex operations on ranges.

std::views::transform is part of the C++20 Standard Library’s view facilities and works with any range, not just those that support the Ranges API. It provides a range adapter that applies a transformation function to each element of a range, producing a new range with the transformed elements. It is simpler to use than std::ranges::transform_view and does not require the use of the Ranges library components.

In terms of performance, there is likely to be little difference between the two algorithms, as they both use lazy evaluation and avoid copying or transforming the range elements until they are actually needed. However, the use of the Ranges library components by std::ranges::transform_view may provide some optimization opportunities in certain cases.

Ultimately, the choice between std::ranges::transform_view and std::views::transform will depend on the specific requirements of your use case. If you are working with ranges that support the Ranges API, std::ranges::transform_view may be the better choice as it provides a more integrated solution. If you are working with ranges that do not support the Ranges API or you want a simpler solution, std::views::transform may be the better choice.

  std::vector<int> vint{ 1, 2, 3, 4, 5, 6, 7, 8, 9 };
  printContainer( "vint data", vint );
  // std::ranges::filter_view
  auto viewEven = std::ranges::views::filter( vint, even ); // No computation performed here!
  // Computation happens in the print function
  printView( "filter_view even", viewEven );
  // Computation on the fly
  // printView( "filter_view odd", std::ranges::filter_view( vint, odd ) );
  printView( "filter_view odd", std::views::filter( vint, odd ) );
  fmt::print( "Transform: " );
  for( int i : vint | std::views::filter( even ) | std::views::transform( square ) ) {
    fmt::print( "{} ", i );
  }
  fmt::print( "\n" );

  auto                        vi            = vint | std::views::filter( even );
  std::ranges::transform_view v_transformed = std::ranges::transform_view( vi, []( int i ) { return i * 10; } );
  printView( "transform_view", v_transformed );
  std::vector<double> vec{ 1.1, 2.2, 4.3, 5.6, 2.4 };
  auto                squared1 = vec | std::ranges::views::transform( []( auto const i ) { return i * i; } );
  // auto squared2 = vec | std::ranges::views::transform(std::sqrt);
  printContainer( "squared1", squared1 );
  // Operate directly on container
  std::vector<int> ns{ 3, 1, 2 };
  printContainer( "ns initial", ns );
  std::ranges::for_each( ns, []( int &n ) { n *= 5; } );
  printContainer( "ns for_each n *= 5", ns );
  std::ranges::sort( ns );
  printContainer( "ns sort", ns );
  std::ranges::sort( ns.begin(), ns.end() ); // BAD
  printContainer( "ns sort BAD", ns );
  std::vector<int> collection{ 2, 6, 8, 40, 64, 70 };
  auto             posMod10 = std::ranges::find_if( collection, []( auto &i ) { return i % 10 == 0; } );
  if( posMod10 != std::end( collection ) ) {
    fmt::print( "Collection contains at least one multiple of 10\n" );
  }
  else {
    fmt::print( "Collection does not contains any multiple of 10\n" );
  }
  // std::ranges::copy
  std::cout << "ranges::copy ";
  std::ranges::copy( collection, std::ostream_iterator<int>( std::cout, " }sep{ " ) );
  std::cout << "\n";
  std::vector<int> v2 = { 1, 11, 23, 131, 2, 3, 4, 5, 6, 7, 8, 9 };
  printContainer( "v2", v2 );

  // 1 11 23 131 2
  std::ranges::take_view v_taken_5 = std::ranges::take_view( v2, 5 );
  printView( "take_view 5 elements", v_taken_5 );

  // 3 4 5 6 7 8 9
  std::ranges::drop_view v_drop_5 = std::ranges::drop_view( v2, 5 );
  printView( "drop_view 5 elements", v_drop_5 );

  // 1 11 23 131
  // number 2 (first not odd) will stop taking
  std::ranges::take_while_view v_taken_while = std::ranges::take_while_view( v2, odd );
  printView( "take_while_view odd", v_taken_while );

  // 2 3 4 5 6 7 8 9
  std::ranges::drop_while_view v_drop_while = std::ranges::drop_while_view( v2, odd );
  printView( "drop_while_view odd", v_drop_while );
  using pair = std::pair<int, std::string>;
  std::vector<pair> numbers{ { 1, "one" }, { 2, "two" }, { 3, "tree" } };

  auto k_view = std::views::keys( numbers );   // 1 2 3
  auto v_view = std::views::values( numbers ); // one two tree

  printView( "k_view", k_view );
  printView( "v_view", v_view );
  std::vector<Student> class_room{ { "Mike", 12 }, { "John", 17 }, { "Drake", 14 }, { "Mary", 16 } };

  std::cout << "Students original order:\n";
  for( auto &s : class_room ) {
    std::cout << s << std::endl;
  }

  std::cout << "Students ordered by age:\n";
  std::ranges::sort( class_room, std::less<>{}, &Student::m_age );
  for( auto &s : class_room ) {
    std::cout << s << std::endl;
  }

  printView(
      "Student under 15", std::views::take_while( class_room, []( const Student &s ) { return s.m_age < 15; } ) );

Possible output

(std::all_of) : All numbers in collection are even
All numbers in collection are even
ints                          : 0 1 2 3 4 5 6 7 8 9 
iota_view{4,10}               : 4 5 6 7 8 9 
views::iota(5, 10)            : 5 6 7 8 9 
vint data                     : 1 2 3 4 5 6 7 8 9 
filter_view even              : 2 4 6 8 
filter_view odd               : 1 3 5 7 9 
Transform: 4 16 36 64 
transform_view                : 20 40 60 80 
squared1                      : 1.2100000000000002 4.840000000000001 18.49 31.359999999999996 5.76 
ns initial                    : 3 1 2 
ns for_each n *= 5            : 15 5 10 
ns sort                       : 5 10 15 
ns sort BAD                   : 5 10 15 
Collection contains at least one multiple of 10
ranges::copy 2 }sep{ 6 }sep{ 8 }sep{ 40 }sep{ 64 }sep{ 70 }sep{ 
v2                            : 1 11 23 131 2 3 4 5 6 7 8 9 
take_view 5 elements          : 1 11 23 131 2 
drop_view 5 elements          : 3 4 5 6 7 8 9 
take_while_view odd           : 1 11 23 131 
drop_while_view odd           : 2 3 4 5 6 7 8 9 
k_view                        : 1 2 3 
v_view                        : one two tree 
Students original order:
Student [ name: Mike, age: 12]
Student [ name: John, age: 17]
Student [ name: Drake, age: 14]
Student [ name: Mary, age: 16]
Students ordered by age:
Student [ name: Mike, age: 12]
Student [ name: Drake, age: 14]
Student [ name: Mary, age: 16]
Student [ name: John, age: 17]
Student under 15              : { name: 'Mike', age: 12 } { name: 'Drake', age: 14 } 

References