C++ Ranges — Part 1
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:
- Definition: Ranges are a set of library concepts and utilities that define a uniform way to access and manipulate sequences of elements, while views are objects that represent a specific subset or transformation of a range.
- Composition: Ranges are designed to be composable, meaning that they can be combined using various operations such as filtering, mapping, and slicing, to create new ranges. Views are similarly composable, but they always operate on an existing range and produce a new view.
- Eager vs Lazy evaluation: Ranges use eager evaluation, which means that the entire sequence is computed upfront when the range is constructed, whereas views use lazy evaluation, which means that the computation is deferred until the values are actually accessed.
- Mutability: Ranges can be either mutable or immutable, depending on the type of range and the underlying container. Views, on the other hand, are always read-only, and cannot modify the underlying range.
- Syntax: Ranges use a set of standardized library concepts
and utilities, such as
std::ranges::range
,std::ranges::view
, andstd::ranges::action
, to define and manipulate ranges. Views use a similar set of concepts and utilities, such asstd::ranges::view_interface
,std::ranges::filter_view
, andstd::ranges::transform_view
, to define and manipulate 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
- A view is a non owning range
- It’s like a window we can set up to view some real data without setting up the infrastructure to store data
- Views are cheap to copy and pass around as function parameters by design.
Range Algorithm
- Legacy algorithm work on iterator pairs
- Range algorithm work on containers directly
- C++ 11
all_of
any_of
none_of
- C++ 20
ranges::all_of
ranges::any_of
ranges::none_of
ranges::sort
ranges::find_if
ranges::copy
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 ) );
std::ranges::filter_view
std::views::filter
std::ranges::transform_view
std::views::transform
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::ranges::views::transform
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 );
std::ranges::for_each
std::ranges::sort
// 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::ranges::find_if
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::ranges::copy
std::cout << "ranges::copy ";
std::ranges::copy( collection, std::ostream_iterator<int>( std::cout, " }sep{ " ) );
std::cout << "\n";
std::ranges::take_view
std::ranges::take_while_view
std::ranges::drop_view
std::ranges::drop_while_view
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 }