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.

Usually we need to find not 1st, but all values. Like in this code:

vector<int>  result;
auto it = vec.begin();

while(it!=vec.end()) {
        it = find_if(it, vec.end(), [=](int i){ return i > val;});
        if (it  == vec.end())  break;
        result.push_back(*it++);
}

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.

Without SCC, minimal complete example for erase snippet vec - 2 would be:

#include <iostream>
#include <string>
using namespace std;

#include <ro/ro.h>
using namespace ro;

int main() {
        vector<int> vec {1,2,3};
        vec - 2;
        for (int x: vec) {
                cout << x << " ";
        }
        cout << endl;
}

With SCC, equivalent which also shows output, is:

scc 'vector<int> vec{1,2,3};  cout << vec - 2;'
{1, 3}

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:

rg  << value     -> rg   //  append value to range
rg1 << rg2       -> rg1  //  append range2 to range1

rg / value       -> it   //  find()
rg / pred        -> it   //  find_if()
rg / rg2         -> rg3  //  search for sub-range

rg - value       -> rg   //  erase all elements equal to value
rg - pred        -> rg   //  erase all elements for which pred evaluated to 'true`
rg - rg2         -> rg   //  erase subrange
rg - it          -> rg   //  erase at iterator

Full list of 1st group operators can be found in Appendix A.

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.

Numeric range

Numeric range is simple numeric sequence generator :

// [0,arg1)  open-ended range. Only 1-arg  range() is open-ended.
scc 'range(5)'
{0, 1, 2, 3, 4}

// [arg1,arg2]  closed range
scc 'range(1,5)'
{1, 2, 3, 4, 5}

// same, with explicit step
scc 'range(1,5,1)'
{1, 2, 3, 4, 5}

// floating point; range type is result of type promotion on all range() arguments
scc 'range(1,5,0.5)'
{1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5}

// negative step
scc 'range(10,0,-1.5)'
{10, 8.5, 7, 5.5, 4, 2.5, 1}

// any arithmetic type
scc 'range('a','z')'
a b c d e f g h i j k l m n o p q r s t u v w x y z

// no need for verbose iota
scc 'vint V = range(5);   V'
{0, 1, 2, 3, 4}

// is lazy, O(1) storage
scc 'auto ints = range(1,999999999999999999l);  *find(ints.begin(), ints.end(), 5)'
5

Iterator range

Iterator range is simple wrapper for pair of iterators.

Assign 42 to elements 2..4 of vector {1,2,3,4,5,6,7,8,9,10}:

scc 'vint V=range(1,10);   range(V/2, V/5) = 42;  V'
{1, 42, 42, 42, 5, 6, 7, 8, 9, 10}

Expression range(1,10) constructs numeric range which initialize V. Expression V/2 returns iterator to 1st element with value 2. Expression range(V/2,V/5) constructs iterator range from pair of iterators.

Ranges can be assigned to another range even if they have different underlying containers. Here is simple lazy-expression range, which shows that it just a reference to underlying container:

scc  'vint V;  auto R=range(V);  V=range(1,10);  R, V'
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

Operation with C-strings

C-strings are treated as special kind of range in RO. The only limitation is that C-string is a POD type and C++ C++ does not allow to overloaded ops on POD types and we need to wrap it into RO range.

We first assign "abc" to a c-string, then append "123" and then remove "bc" substring :

scc 'char s[99] = "abc";  (range(s) << "123") - "bc"'
a123

Same example, but for str::string (without range-wrapping):

scc 'str s("abc");  (s << "123") - "bc"'
a123

FP operators: Map, Reduce, Filter

Map/Transform

Operator ro::operator* (Range,Functor) is map/transform operator, where Functor have signature compatible with return_type(*)(range_elem_type).

First examples:

scc 'range("abc") * toupper'
ABC

scc 'vint V{-1,0,1};  V * abs'
{1, 0, 1}

In above, toupper is defined in functor.h, abs is taken from std::.

Expression V * abs is equivalent to:

std::transform(V.begin(), V.end(), V.begin(), std::abs<int>)'

Example where transformed type is different from source range type:

scc 'vector<const char*> V{"aa", "bbb"};  V * strlen'
{2, 3}

Length of longest string:

scc 'vector<str> V{"aa", "bbb", "c"};  V * size || max'
3

Functor size_t ro::size(Range) is similar to strlen but will work with any Range.

Fold

Operator ro::operator|| (Range,Functor) is fold operator (aka reduce, accumulate, compress, catamorphisms, or inject).

scc 'vint V{1,2,3};  V || plus<int>()'
6

scc 'vint V{1,2,3};  V || add'                  // add -  similar to std::plus<T>(), but will handle empty ranges too
6

scc 'range(1,3) || mul'                         // fuctorial;   mul -  similar to std::multiplies<T>()
6

scc 'vstr V{"aa", "bb", "cc"};  V || add'       // concatenates
aabbcc

scc 'vint V{1,2,3};  V || std::min'
1

scc 'vstr V{"aaa", "bb", "cccc"};  V * size ||  min'    // length of shortest string
2

Expression V || min is equivalent to:

std::accumulate (
        std::next(V.begin()),
        V.end(),
        V.front(),
        static_cast<const int& (*)(const int&, const int&)>(std::min<int>)
);

Integrate sin(x) from 0 to pi (compare with WolframAlpha):

scc 'auto d=0.001;  (range(0,pi,d) * sin || add) * d'
2

Fold evaluated as left-fold where init value is identity element for performed operation.

Pipe

Pipe operator have much more freedom on what it can do with range. It can filter out, resize, reorder and convert to different type. Performed operation depends on type of right-hand-side object.

Filter: range|predicate

Filters have two signatures: operators|(Range, Predicate) and operator|(Range, elem_type).

Predicate is functor with signature compatible with bool(*)(elem_type). When it used with pipe operator, all elements which predicate evaluates to false - are filtered out:

scc 'range("abc-123") | isdigit'
123

Filter and transform:

scc '(range("abc-123, xyz/") | isalnum) * toupper'
ABC123XYZ

Hide digits:

scc "str S=\"John Q Public  (650)1234567\";  S|isdigit='X';  S"
John Q Public  (XXX)XXXXXXX

Expression S | isdigit, produce a filtered range, where isdigit is predicate for filtering. This is lazy-evaluated, it does not creates temporary container.

Filter operator|(Range, range_elem_type) selects only elements which are equal to second argument.

Count number of 2 elements, then replace 2 with 42:

scc 'vint V{2,1,2,3};   size(V|2)'
2

scc 'vint V{2,1,2,3};  V|2 = 42;  V'
{42, 1, 42, 3}

Lazy gotcha:

scc 'vint V{2,1,2,3};  (V|2 = 42)'
{}

Why empty range? Lets do it slowly:

vector<int> V{2,1,2,3};

auto twos = V|2;    // Filtered range of V,
                    // where elements are predicated on [](int x)->bool {return x==2;}.
                    // Range twos does not hold any elements. Value of twos is expression V|2.
                    // That is, twos holds reference to V and to filtering predicate

cout << twos;       // {2,2} - twos lazily evaluates expression V|2

twos = 42;          // Assign to every element of V which had element equal to 2.
                    // But value of twos didn't change, it is still expression V|2.

cout << V;          // {42,1,42,3}
cout << (V|2);      // {} - empty range, because there are no 2 in V!.
cout << twos;       // {} - same thing.

Evaluate: range|stl-algorithm

Simple operator|(Range, void(*)(It,It)) -> Range matches STL algorithm which have 2 argument iterators:

scc 'vint{2,3,1} | std::sort | std::reverse'
{3, 2, 1}

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:

scc 'vint V{2,3,1};  std::sort(+V,-V);  std::reverse(+V,-V);  V'
{3, 2, 1}

Unary + and - corresponds to members begin() and end().

Another signature which can be matched is operator|(Range, It(*)(It,It)) - this can match algorithms like std::unique:

scc 'vint{3,1,2,3} | std::sort | std::unique'
{1, 2, 3}

Which is equivalent to:

scc 'vint V{3,1,2,3};  std::sort(+V,-V);  auto e=std::unique(+V, -V);  V.erase(e,-V);  V'
{1, 2, 3}

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

  • clear(range) -> void

  • all ctype functions reimplemented

  • add,sub, mul and div - function object equivalent to std::plus<T>(), etc

Simple IO

Bar-print

Instead of std::cout printing, in SCC you can use so called bar-print. Below are bar-print statements with equivalent code in comments:

_  x;                  //  cout << x;
__ x;                  //  cout << x << endl;
__ x, y;               //  cout << x << " " << y << endl;

On last line " " will not be printed if x or y are strings.

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:

copy(V.begin(), V.end(), ostream_iterator<int>   (cout, " "));
copy(S.begin(), S.end(), ostream_iterator<string>(cout, " "));

Include io.h defines oi object - SCC replacement for ostream_iterator, which can work with any type and is pre-defined. With oi, above example will be:

copy(V.begin(), V.end(), oi);
copy(S.begin(), S.end(), oi);

Same using range operators:

copy(+V, -V, oi);
copy(+S, -S, oi);

Also assigning a container to oi is equivalent to doing std::copy, so still shorter:

oi = V;
oi = S;

STL Containers input / output

Any STL container can be also printed without oi. You can just send it to std::cout or bar-print it.

In below example __ C; can be replaced with cout << C;. Equivalent code and output is shown in comments:

vint            C {1,2,3};        __ C;    //  vector<int> C{1,2,3};
                                           //  cout << "{";
                                           //  for(auto it=C.begin(); it!=C.end()-1; it++)
                                           //       cout << *it << ", ";
                                           //  if(!C.empty()) cout << C.back();
                                           //  cout << "}\n";
                                           //  prints: {1, 2, 3}

int             C[] {1,2,3};       __ C;   //  {1,2,3}
array<int,3>    C {1,2,3};         __ C;   //  {1,2,3}
tuple<int,int>  C {1,2};           __ C;   //  ⟨1, 2⟩
map<int,int>    C {{1,2},{3,4}};   __ C;   //  {⟨1,2⟩, ⟨3,4⟩}
vector<vint>    C {{1,2},{3,4}};   __ C;   //  {{1, 2}, {3, 4}}

Same for input:

echo 1 2    | scc 'vint            C;     cin >> C;   C'        //  {1,2}
echo 1 2    | scc 'int             C[2];  cin >> C;   C'        //  {1,2}
echo 1 2    | scc 'tuple<int,int>  C;     cin >> C;   C'        //  ⟨1,2⟩
echo 1 2    | scc 'array<int,2>    C;     cin >> C;   C'        //  {1,2}
echo 1 2    | scc 'set<int>        C;     cin >> C;   C'        //  {1,2}
echo 1 2 3 4| scc 'map<int,int>    C;     cin >> C;   C'        //  {⟨1,2⟩, ⟨3,4⟩}

If a container have non-zero length, then the corresponding number of elements will be read. If container is empty then input will be EOF-terminated.

Object IN

Pre-defined object in can be used as value in expression which is read from std::cin.

int i(in);                      //  int i;  cin >> i;
float x = 1.1 + float(in);      //  float y;  cin >> y;   float x = 1.1 + y;
Warning Object in does not check for EOF. Use only in context where you can ignore or do not expect EOF.

To input a container:

vint V = in(10);
vint W = in;            // Till EOF.  Note:  we can not use:  vint W(in);

Here is example of C++ vs SCC input where we need to read a number and than corresponding number of container elements:

int N;
cin >> N;
vector<int> V(N);
for(int i=0; i<N; i++)
        cin >> V[i];

With object in:

vint V = in(in);

IO.H - Standalone Use

File io.h can be used as standalone, independent include.

#include <io.h>
int main() {
        __ "hello world";
}

Includes simple.h, cj.h and stl.h can also be used independently.

Install

git clone http://github.com/lvv/ro /PATH/TO/INSTALL/RO/

echo '
        #include <ro/ro.h>
        using namespace ro;
        int main() {
                return range(10) || add;
        }
' > /tmp/ro-test.cc

# test compile
CXXFLAGS+='-I/PATH/TO/INSTALL/ -std=c++11' make /tmp/ro-test  &&  /tmp/ro-test

If you install in includes default-searchable directory (like /usr/include or /usr/local/include), you do not need to specify -I /path/to/install/ compiler option.

For CLANG use following options:

CXX=clang++
CXXFLAGS+="-D__STRICT_ANSI__  -std=c++0x "

To be able to run examples you need to install SCC too.

image::include-deps.png

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.

                        //  This is just approximate semantic, not exact impl
+rg                     //  rg.begin()
-rg                     //  rg.end()
++rg                    //  rg.front()
rg++                    //  rg.back()
--rg                    //  rg.pop_front()
rg--                    //  rg.pop_back()

rg << value             //  rg.push_back(value)  or similar in semantic operators
value >> rg             //  rg.push_front(value)
rg >> value             //  value = rg.back(value);   rg.pop_back();
value << rg             //  value = rg.front(value);  rg.pop_front();
rg1 << rg2              //  copy(rg2.begin(), rg2.end(), back_inserter(rg1));
rg1 >> rg2              //  copy(rg1.rbegin(), rg1.rend(), front_inserter(rg2));

++pair                  //  pair.first
pair++                  //  pair.second

++tuple                 //  get<0>(tuple)
tuple++                 //  get<tuple_size<tuple<Types...> >::value-1>(tuple)


stack << value          //  stack.push(value)
stack++                 //  stack.top()
stack--                 //  stack.pop()
stack >> value          //  value = stack.top(value);  stack.pop()

queue++                 //  queue.back()
++queue                 //  queue.front()
--queue                 //  queue.pop()
queue << value          //  queue.push(value)
value << queue          //  value = queue.front(value);  queue.pop()

rg = rg2                //  underlying containers can be of different type
rg = value              //  fill(rg.begin(), rg.end(), value)

// SEARCH

rg / value              // find(+rg, -rg, value)
rg % value              // find(+rg, -rg, value) != -rg
rg / Pred               // find_if(+rg, -rg, pred)
rg % Pred               // find_if(+rg, -rg, pred) != -rg
rg / rg                 // search(+rg, -rg, +rg, -rg)


// GENERIC ERASE

rg - value              // erase all elements equal to value
rg - pred               // erase all elements for which pred evaluated to 'true`
rg - rg2                // erase subrange
rg - it                 // erase at iterator