think-cell Suite ha llegado. Descubra su biblioteca y nuevas herramientas.

Compartir

+ - 0:00:00
Notes for current slide
Notes for next slide

From Iterators To Ranges

The Upcoming Evolution Of the Standard Library

1 / 64

Questions

  • Who knows Boost.Range?
  • Who knows Eric Niebler's Range V3 library?
  • Who uses ranges in everyday programming?
2 / 64

Why Ranges?

std::vector<T> vec=...;
std::sort( vec.begin(), vec.end() );
vec.erase( std::unique( vec.begin, vec.end() ), vec.end() );

How often do we have to mention vec?

Pairs of iterators belong together -> put into one object!

tc::unique_inplace(tc::sort(vec));

Much nicer!

3 / 64

Why do I think I know something about ranges?

  • think-cell has a range library
    • evolved from Boost.Range
  • 1 million lines of production code use it
  • Library and production code evolve together
    • ready to change library and production code anytime
    • no obstacle to library design changes
    • large code base to try them out
4 / 64

Ranges in C++11

  • range-based for

    for( int& i : <range_expression> ) {
    ...
    }
  • universal access to begin/end

    std::begin/end(<range_expression>)
5 / 64

Ranges in C++11

  • range-based for

    for( int& i : <range_expression> ) {
    ...
    }
  • universal access to begin/end

    std::begin/end(<range_expression>)
  • that's all ... :-(
6 / 64

Future of Ranges

  • Eric Niebler's pet project

  • Ranges TS ("Technical Specification")

    • <algorithm> supports ranges
namespace ranges {
template< typename Rng, typename What >
decltype(auto) find( Rng && rng, What const& what ) {
return std::find(
ranges::begin(rng),
ranges::end(rng),
what
);
}
}
  • Range V3
    • preview of what Eric wants to standardize
7 / 64

What are Ranges?

  • Containers
    vector
    string
    list
    • own elements
    • deep copying
      • copying copies elements in O(N)
    • deep constness
      • const objects implies const elements
8 / 64

What are Ranges?

  • Containers
    vector
    string
    list
    • own elements
    • deep copying
      • copying copies elements in O(N)
    • deep constness
      • const objects implies const elements
  • Views
Range
/\
/ \
/ \
Container View
9 / 64

Views

template<typename It>
struct iterator_range {
It m_itBegin;
It m_itEnd;
It begin() const {
return m_itBegin;
}
It end() const {
return m_itEnd;
}
};
  • reference elements
  • shallow copying
    • copying copies reference in O(1)
  • shallow constness
    • view object const independent of element const
10 / 64

More Interesting Views: Range Adaptors

std::vector<int> v;
auto it=ranges::find(
v,
4
); // first element of value 4.

vs.

struct A {
int id;
double data;
};
std::vector<A> v={...};
auto it=ranges::find_if(
v,
[](A const& a){ return a.id==4; } // first element of value 4 in id
);
  • Similar in semantics
  • Not at all similar in syntax
11 / 64

Transform Adaptor

std::vector<int> v;
auto it=ranges::find(
v,
4
); // first element of value 4.

vs.

struct A {
int id;
double data;
};
std::vector<A> v={...};
auto it=ranges::find(
tc::transform(v,std::mem_fn(&A::id)),
4
); // first element of value 4 in id
12 / 64

Transform Adaptor (2)

struct A {
int id;
double data;
};
std::vector<A> v={...};
auto it=ranges::find(
tc::transform(v,std::mem_fn(&A::id)),
4
); // first element of value 4 in id

What is it pointing to?

13 / 64

Transform Adaptor (2)

struct A {
int id;
double data;
};
std::vector<A> v={...};
auto it=ranges::find(
tc::transform(v,std::mem_fn(&A::id)),
4
); // first element of value 4 in id

What is it pointing to?

  • int!
14 / 64

Transform Adaptor (2)

struct A {
int id;
double data;
};
std::vector<A> v={...};
auto it=ranges::find(
tc::transform(v,std::mem_fn(&A::id)),
4
); // first element of value 4 in id

What is it pointing to?

  • int!

What if I want it to point to A?

15 / 64

Transform Adaptor (2)

struct A {
int id;
double data;
};
std::vector<A> v={...};
auto it=ranges::find(
tc::transform(v,std::mem_fn(&A::id)),
4
); // first element of value 4 in id

What is it pointing to?

  • int!

What if I want it to point to A?

auto it=ranges::find(
tc::transform(v,std::mem_fn(&A::id)),
4
).base();
16 / 64

Transform Adaptor Implementation

template<typename Base, typename Func>
struct transform_range {
struct iterator {
private:
Func m_func; // in every iterator, hmmm...
decltype( tc::begin(std::declval<Base&>()) ) m_it;
public:
decltype(auto) operator*() const {
return m_func(*m_it);
}
decltype(auto) base() const {
return (m_it);
}
...
};
};
17 / 64

Filter Adaptor

Range of all a with a.id==4?

tc::filter(
v,
[](A const& a){ return 4==a.id; }
);
  • Lazy! Filter executed while iterating
18 / 64

Filter Adaptor Implementation

template<typename Base, typename Func>
struct filter_range {
struct iterator {
private:
Func m_func; // functor and TWO iterators!
decltype( ranges::begin(std::declval<Base&>()) ) m_it;
decltype( ranges::begin(std::declval<Base&>()) ) m_itEnd;
public:
iterator& operator++() {
++m_it;
while( m_it!=m_itEnd
&& !static_cast<bool>(m_func(*m_it)) ) ++m_it;
// why static_cast<bool> ?
return *this;
}
...
};
};
19 / 64

How does iterator look like of

tc::filter(tc::filter(tc::filter(...))) ?

20 / 64
m_func3
m_it3
m_func2
m_it2
m_func1
m_it1;
m_itEnd1;
m_itEnd2
m_func1
m_itEnd1;
m_itEnd1;
m_itEnd3
m_func2
m_it2
m_func1
m_itEnd1;
m_itEnd1;
m_itEnd2
m_func1
m_itEnd1;
m_itEnd1;

Boost.Range does this! ARGH!

21 / 64

More Efficient Range Adaptors

Must keep iterators small

Idea: adaptor object carries everything that is common for all iterators

m_func
m_itEnd

Iterators carry reference to adaptor object (for common stuff) and base iterator

*m_rng
m_it
22 / 64

More Efficient Range Adaptors

Must keep iterators small

Idea: adaptor object carries everything that is common for all iterators

m_func
m_itEnd

Iterators carry reference to adaptor object (for common stuff) and base iterator

*m_rng
m_it
  • Iterator cannot outlive its range
23 / 64

Again: How does iterator look like of

tc::filter(tc::filter(tc::filter(...))) ?
m_rng3
m_it3
m_rng2
m_it2
m_rng1
m_it1
  • Range V3 State of The Art
  • Still not insanely great...
24 / 64

Index Concept

Index

  • Like iterator
  • But all operations require its range object
template<typename Base, typename Func>
struct index_range {
...
using Index=...;
Index begin_index() const;
Index end_index() const;
void increment_index( Index& idx ) const;
void decrement_index( Index& idx ) const;
reference dereference( Index& idx ) const;
...
};
25 / 64

Index-Iterator Compatibility

  • Index from Iterator

    • using Index = Iterator
    • Index operations = Iterator operations
  • Iterator from Index

template<typename IndexRng>
struct iterator_for_index {
IndexRng* m_rng
typename IndexRng::Index m_idx;
iterator& operator++() {
m_rng.increment_index(m_idx);
return *this;
}
...
};
26 / 64

Super-Efficient Range Adaptors With Indices

Index-based filter_range

template<typename Base, typename Func>
struct filter_range {
Func m_func;
Base& m_base;
using Index=typename Base::Index;
void increment_index( Index& idx ) const {
do {
m_base.increment_index(idx);
} while( idx!=m_base.end_index()
&& !m_func(m_base.dereference_index(idx))
);
}
};
27 / 64

Super-Efficient Range Adaptors With Indices

Index-based filter_range

template<typename Base, typename Func>
struct filter_range {
Func m_func;
Base& m_base;
using Index=typename Base::Index;
...
template<typename IndexRng>
struct iterator_for_index {
IndexRng* m_rng
typename IndexRng::Index m_idx;
...
  • All iterators are two pointers
    • irrespective of stacking depth
28 / 64

Range V3 and rvalue containers

If adaptor input is lvalue container

  • view::filter creates view
  • view is reference, O(1) copy, shallow constness etc.
auto rng=view::filter(vec,pred1);
bool b=ranges::any_of(rng,pred2);
29 / 64

Range V3 and rvalue containers

If adaptor input is rvalue container

  • view::filter cannot create view
  • view would hold dangling reference to rvalue
auto rng=view::filter(create_vector(),pred1); // DOES NOT COMPILE
bool b=ranges::any_of(rng,pred2);
30 / 64

Range V3 and rvalue containers

If adaptor input is rvalue container

  • view::filter cannot create view
  • view would hold dangling reference to rvalue
auto rng=action::filter(create_vector(),pred1); // COMPILES
bool b=ranges::any_of(rng,pred2);
31 / 64

Range V3 and rvalue containers

If adaptor input is rvalue container

  • view::filter cannot create view
  • view would hold dangling reference to rvalue
auto rng=action::filter(create_vector(),pred1); // COMPILES
bool b=ranges::any_of(rng,pred2);

Big Trap

  • not lazy anymore!
32 / 64

think-cell and rvalue containers

If adaptor input is lvalue container

  • tc::filter creates view
  • view is reference, O(1) copy, shallow constness etc.

If adaptor input is rvalue container

  • tc::filter creates container
  • aggregates rvalue container, deep copy, deep constness etc.

Always lazy

  • Laziness and container-ness are orthogonal concepts
auto rng=tc::filter(vec,pred1);
bool b=ranges::any_of(rng,pred2);
auto rng=tc::filter(creates_vector(),pred1);
bool b=ranges::any_of(rng,pred2);
33 / 64

Beyond Range V3:

More Flexible Algorithm Returns

template< typename Rng, typename What >
decltype(auto) find( Rng && rng, What const& what ) {
auto const itEnd=ranges::end(rng);
for( auto it=ranges::begin(rng); it!=itEnd; ++it )
if( *it==what )
return it;
return itEnd;
}
34 / 64

More Flexible Algorithm Returns (2)

template< typename Pack, typename Rng, typename What >
decltype(auto) find( Rng && rng, What const& what ) {
auto const itEnd=ranges::end(rng);
for( auto it=ranges::begin(rng); it!=itEnd; ++it )
if( *it==what )
return Pack::pack(it,rng);
return Pack::pack_singleton(rng);
}
struct return_iterator_or_end {
static auto pack(auto it, auto&& rng) {
return it;
}
static auto pack_singleton(auto && rng) {
return ranges::end(rng);
}
}
auto it=find<return_iterator_or_end>(...)
35 / 64

More Flexible Algorithm Returns (3)

template< typename Pack, typename Rng, typename What >
decltype(auto) find( Rng && rng, What const& what ) {
auto const itEnd=ranges::end(rng);
for( auto it=ranges::begin(rng); it!=itEnd; ++it )
if( *it==what )
return Pack::pack(it,rng);
return Pack::pack_singleton(rng);
}
struct return_iterator {
static auto pack(auto it, auto&& rng) {
return it;
}
static auto pack_singleton(auto && rng) {
std::assert(false);
return ranges::end(rng);
}
}
auto it=find<return_iterator>(...)
36 / 64

More Flexible Algorithm Returns (4)

template< typename Pack, typename Rng, typename What >
decltype(auto) find( Rng && rng, What const& what ) {
auto const itEnd=ranges::end(rng);
for( auto it=ranges::begin(rng); it!=itEnd; ++it )
if( *it==what )
return Pack::pack(it,rng);
return Pack::pack_singleton(rng);
}
struct return_head {
static auto pack(auto it, auto&& rng) {
return tc::take( std::forward<decltype(rng)>(rng), it);
}
static auto pack_singleton(auto&& rng) {
std::assert(false);
return tc::take( std::forward<decltype(rng)>(rng), ranges::begin(rng));
}
}
auto it=find<return_head>(...)
37 / 64

Generator Ranges

template<typename Func>
void traverse_widgets( Func func ) {
if( window1 ) {
window1->traverse_widgets(std::ref(func));
}
func(button1);
func(listbox1);
if( window2 ) {
window2->traverse_widgets(std::ref(func));
}
}
  • like range of widgets
  • but no iterators
38 / 64

Generator Ranges

template<typename Func>
void traverse_widgets( Func func ) {
if( window1 ) {
window1->traverse_widgets(std::ref(func));
}
func(button1);
func(listbox1);
if( window2 ) {
window2->traverse_widgets(std::ref(func));
}
}
mouse_hit_any_widget=tc::any_of(
[](auto func){ traverse_widgets(func); },
[](auto const& widget) {
return widget.mouse_hit();
}
);
39 / 64

External Iteration

  • Consumer calls producer to get new element
  • example: C++ iterators
^
| Stack Producer Producer
| / \ / \
Consumer Consumer Consumer
  • Consumer is at bottom of stack
  • Producer is at top of stack
40 / 64

External iteration (2)

Consumer is at bottom of stack

  • contiguous code path for whole range
  • easier to write
  • better performance
    • state encoded in instruction pointer
    • no limit for stack memory

Producer is at top of stack

  • contiguous code path for each item
  • harder to write
  • worse performance
    • single entry point, must restore state
    • fixed amount of memory or go to heap
41 / 64

Internal Iteration

  • Producer calls consumer to offer new element
  • example: EnumThreadWindows
^
| Stack Consumer Consumer
| / \ / \
Producer Producer Producer

Producer is at bottom of stack

  • ... all the advantages of being bottom of stack ...

Consumer is at top of stack

  • ... all the disadvantages of being top of stack ...
42 / 64

Coroutines

Can both consumer and producer be bottom-of-stack?

  • Yes, with coroutines
// does not compile, conceptual
generator<widget&> traverse_widgets() {
if( window1 ) {
window1->traverse_widgets();
}
yield button1;
yield listbox1;
if( window2 ) {
window2->traverse_widgets();
}
}
43 / 64

Coroutines (2)

  • Stackful
    • use two stacks and switch between them
    • very expensive
      • implemented as OS fibers
      • 1 MB of virtual memory per coroutine
  • Stackless (N4402)
    • can only yield in top-most function
// does not compile, conceptual
generator<widget&> traverse_widgets() {
if( window1 ) {
window1->traverse_widgets();
}
yield button1;
yield listbox1;
if( window2 ) {
window2->traverse_widgets();
}
}
44 / 64

Coroutines (2)

  • Stackful
    • use two stacks and switch between them
    • very expensive
      • implemented as OS fibers
      • 1 MB of virtual memory per coroutine
  • Stackless (N4402)
    • can only yield in top-most function
    • still a bit expensive
      • dynamic jump to resume point
      • save/restore some registers
      • no aggressive inlining
45 / 64

Internal Iteration often good enough

Algorithm Internal Iteration?
find no (single pass iterators)
binary_search no (random access iterators)
46 / 64

Internal Iteration often good enough

Algorithm Internal Iteration?
find no (single pass iterators)
binary_search no (random access iterators)
for_each yes
accumulate yes
all_of yes
any_of yes
none_of yes
...
47 / 64

Internal Iteration often good enough

Algorithm Internal Iteration?
find no (single pass iterators)
binary_search no (random access iterators)
for_each yes
accumulate yes
all_of yes
any_of yes
none_of yes
...
Adaptor Internal Iteration?
tc::filter yes
tc::transform yes

So allow ranges that support only internal iteration!

48 / 64

any_of implementation

namespace tc {
template< typename Rng >
bool any_of( Rng const& rng ) {
bool bResult=false;
tc::enumerate( rng, [&](bool_context b){
bResult=bResult || b;
} );
return bResult;
}
}
  • tc::enumerate is common interface for iterator, index and generator ranges

  • Ok?

49 / 64

any_of implementation

namespace tc {
template< typename Rng >
bool any_of( Rng const& rng ) {
bool bResult=false;
tc::enumerate( rng, [&](bool_context b){
bResult=bResult || b;
} );
return bResult;
}
}
  • tc::enumerate is common interface for iterator, index and generator ranges

  • Ok?

    • std::any_of stops when true is encountered!
50 / 64

Interruptable Generator Ranges

First idea: exception!

51 / 64

Interruptable Generator Ranges

First idea: exception!

  • too slow:-(
52 / 64

Interruptable Generator Ranges

First idea: exception!

  • too slow:-(

Second idea:

enum break_or_continue {
break_,
continue_
};
template< typename Rng >
bool any_of( Rng const& rng ) {
bool bResult=false;
tc::enumerate( rng, [&](bool_context b){
bResult=bResult || b;
return bResult ? break_ : continue_;
} );
return bResult;
}
53 / 64

Interruptable Generator Ranges (2)

  • Generator Range can elide break_ check
    • If functor returns break_or_continue,
      • break if break_ is returned.
    • If functor returns anything else,
      • nothing to check, always continue
54 / 64

concat

std::list<int> lst;
std::vector<int> vec;
std::for_each( tc::concat(lst,vec), [](int i) {
...
});
55 / 64

concat implementation with indices

template<typename Rng1, typename Rng2>
struct concat_range {
private:
using Index1=typename range_index<Rng1>::type;
using Index2=typename range_index<Rng2>::type;
Rng1& m_rng1;
Rng2& m_rng2;
using index=std::variant<Index1, Index2>;
public:
...
56 / 64

concat implementation with indices (2)

...
void increment_index(index& idx) {
idx.switch(
[&](Index1& idx1){
m_rng1.increment_index(idx1);
if (m_rng1.at_end_index(idx1)) {
idx=m_rng2.begin_index();
}
},
[&](Index2& idx2){
m_rng2.increment_index(idx2);
}
);
}
...
  • Branch for each increment!
57 / 64

concat implementation with indices (3)

...
auto dereference_index(index const& idx) const {
return idx.switch(
[&](Index1 const& idx1){
return m_rng1.dereference(idx1);
},
[&](Index2 const& idx2){
return m_rng2.dereference(idx2);
}
);
}
...
};
  • Branch for each dereference!
  • How avoid all these branches?
58 / 64

concat implementation with indices (3)

...
auto dereference_index(index const& idx) const {
return idx.switch(
[&](Index1 const& idx1){
return m_rng1.dereference(idx1);
},
[&](Index2 const& idx2){
return m_rng2.dereference(idx2);
}
);
}
...
};
  • Branch for each dereference!
  • How avoid all these branches?
    • With Generator Ranges!
59 / 64

concat implementation as generator range

template<typename Rng1, typename Rng2>
struct concat_range {
private:
Rng1 m_rng1;
Rng2 m_rng2;
public:
...
// version for non-breaking func
template<typename Func>
void operator()(Func func) {
tc::fn_enumerate(m_rng1, func);
tc::fn_enumerate(m_rng2, func);
}
};
  • Even iterator-based ranges sometimes perform better with generator interface!
60 / 64

Now that we have all this range stuff

61 / 64

Now that we have all this range stuff

I hate the range-based for loop!

62 / 64

Now that we have all this range stuff

I hate the range-based for loop!

because it encourages people to write this

bool b=false;
for( int n : rng ) {
if( is_prime(n) ) {
b=true;
break;
}
}
63 / 64

Now that we have all this range stuff

I hate the range-based for loop!

because it encourages people to write this

bool b=false;
for( int n : rng ) {
if( is_prime(n) ) {
b=true;
break;
}
}

instead of this

bool b=tc::any_of( rng, is_prime );

THANK YOU!

64 / 64

Questions

  • Who knows Boost.Range?
  • Who knows Eric Niebler's Range V3 library?
  • Who uses ranges in everyday programming?
2 / 64
Paused

Help

Keyboard shortcuts

, , Pg Up, k Go to previous slide
, , Pg Dn, Space, j Go to next slide
Home Go to first slide
End Go to last slide
b / m / f Toggle blackout / mirrored / fullscreen mode
c Clone slideshow
p Toggle presenter mode
t Restart the presentation timer
?, h Toggle this help
Esc Back to slideshow