RO — Range Operators

About

RO is C++ headers only library, which provides alternative interface to
STL, mostly through overloaded operators and with some elements of functional programming. RO operates on ranges
which are STL containers, RO-ranges, Tuples, C-arrays and C-strings.

RO strives for simpler and more expressive C++ code with zero overhead.

Introduction

Some consider that use of overloaded operators, not having meaningful names and having
multiple overloads are confusing and ambiguous. I will try to show that it
is not so. Below follow examples comparing STL vs RO code.

In below RO code snippet, you can probably figure out what it does without explanation:

container << value1 << value2;

Yes, this was like applying two push_back (if container was vector-like). That was simple.

If you want to add two values to a container with STL - that would be not so simple. And I am not talking about
syntax:

If above container is std::vector - we will use push_back;

If std::set - we need insert;

If std::map - operator[] or insert;

If std::stack - that would be push;

If value1 is container too, you will need std::copy possibly with std::back_insert_iterator.
Or std::slice. Or plain for-loop;

If container is a C-string, you will need to close your STL reference and start looking at man string;

There is logical explanation for STL’s non-uniformity. Operations
performed under the hood are very different for each case. End result is that
even after many years of using it, I still need STL reference manual handy.

With RO, above snippet won’t change for any of above listed cases.
RO library has same interface for STL containers, C-strings, C-arrays and RO-Ranges.

In next example we need to perform another simple operation: remove a value from a
std::vector:

vec.erase(stl::remove(vec.begin(), vec.end(), x), vec.end());

For above you need not only STL reference, but you will need know erase-remove idiom.
If container is not std::vector, you will have to find out how STL
does erase for your particular container.

With RO you won’t need expression with calls to 4 member function, 1
algorithm and 10 parenthesis:

vec - x;

And again, above expression won’t change if we use any container (or range) and if
x is a value, a predicate, iterator or sub-range.

Some will say that STL iterface is more flexible because we can erase sub-range defined by two iterators.
No problem, RO has plenty of flexibility:

vec - range(it1,it2);

One more example. This was used by someone as example of terseness and
simplicity of C++11. We need to find an element bigger than val:

find_if(vec.begin(), vec.end(), [=](int i){ return i > val})

With RO (using RO’s lambdas):

vec / (_1 > val)

Again, with STL, you will need different algorithm if you are looking for a value
or sub-range. With RO, that would be the same divide op: v / something.

This is rather low level way of doing things. RO provides mulch simpler code with filtered range:

vector<int> result = vec | (_1>val);

One of the reasons why RO chooses to use operators was because it was designed
to be composable. Range/functors/RO-lambdas can be combined into complex
expression, where actions usually flow from left to right. Like in classic Unix pipe:

some_range | std::sort | std::unique | std::reverse;

That was valid RO expression. If RO used functions instead of operators, this
would be much more complex expression with 4-level deep parenthesis.

SCC

RO was originally part of SCC ( C++
snippet evaluator at shell prompt). RO does not depends on SCC but we will use
it in examples, so we need short intro for SCC.

1st line in above is what you type at shell prompt. 2nd line is output.
SCC includes RO and all standard libraries includes; and it sends last
statement-expression, if not terminated by semi-colon, to std::cout.

In examples we will use two SCC’s types typedefs: str is std::string, and
vint is std::vector<int>. We need these because most examples are
1-liners. With these we can rewrite above as:

scc 'vint{1,2,3} - 2'
{1, 3}

RO print operator (and std::cout) can print almost any STL objects
directly. RO’s print operators are _ and __. Longer one terminates output with endl.
Items in print operator are separated by , not by <<.

scc '__ range(3), endl, make_tuple("abc",42);'
{0, 1, 2}
⟨abc, 42⟩

Or equivalent:

scc '__ range(3); __ make_tuple("abc",42)'
{0, 1, 2}
⟨abc, 42⟩

This is all you need to know about SCC. You can read more about it on
SCC page. To be able to run examples you
need to install RO and
SCC

Ranges and Range Operators

Ranges are generalization of STL containers.
They are RO ranges, STL containers, C-arrays and C-strings. RO defines
C-strings as zero terminated arrays of char (not signed char or unsigned chars).

RO have two groups of operators. First provides simple container management
functions. This is roughly equivalent to STL container member functions.
You’ve already seen some ops from this group:

Second is more complex operations like
fold, reduce, filter and arbitrary transformation of ranges.

RO Ranges

RO Ranges are ranges that have extended but STL compliant interface. They
also often do not contain range elements data, they refer to an existing STL
container or generate its elements.

RO Ranges are: iterator range, lazy-expression range and numeric range.
Lazy means that its elements are calculated at the moment when range
element is accessed. Exact type of the range often is not important - these
are types mostly of temporaries.

Evaluate: range|stl-algorithm

Unlike other operators, it does not return new lazy-expression-range object,
but the same range that was on input, modified in place (and requires mutable
range) by corresponding stl-algorithm. Above example is equivalent to:

RO Functors

Functors are function object, lambdas and plain functions.

Regrettably not all function, are usable with RO.

For example RO relies on fact that functor have argument of the same type as
range element type. But for examples ctype.h functions, which work with
c-strings, have not char but int as parameter and they can be defined as
macros. And what makes it even worse, they are randomly injected into default
namespace by LIBSTDC++ . As workaround RO put equivalent functions
with correct signatures into ro namespace.

Also, even if we do not use using-directive or using-declaration, C++11
inject into default namespace old C names. C++11 comity made this
compiler defect a standard. For example: <string> includes <cstdlib> which
define in default namespace div. So we are forced to use our div functor
with ro::.

Also compiler have its own peculiarities. Following compiles with clang, but not with gcc:

scc 'map<int,int> M{{1,11}, {2,22}}; M * get<0>'
{1, 2}

RO also defines some generic helper functions:

size(range) -> size_t

empty(range) -> bool

endz(range) -> It — like std::end(StlContainer), but will work with C-strings too

On exit, SCC also checks if last output was unterminated with linefead. If
true, it will add a linefead. Let say if last statement was cout << x;, then SCC
will add cout << endl; on exit.

OI - Generic ostream_iterator

Standard library includes std::ostream_iterator. Unlike std::cout it
accepts only one specific type (specified at construction time) and it is not
pre-defined object. So to use it with STL algorithms, you need
call its quite verbose constructor.

Let say we have vector<int> V; and set<string> S; and we want
to print these. With std::ostream_iterator:

Appendix A

In table below are 1st group - simple shortcuts for container member functions.
Unlike STL member functions (which often return void), they usually return
result of operation (so it can be used in expressions). In comments shown
semantic, not exact operation which will be performed - these might be very
different for different type of range.