ABOUT

RO is C++ headers only library, which provides alternative interface to STL with some elements of functional programming.

RO operates on ranges which are STL containers, RO-ranges, Tuples, C-arrays or C-strings. Most of operations are provided through overloaded operators.

Some consider that overloaded operators, not having meaningful names and having multiple overloads are confusing and ambiguous.

I would argue that it is not so.

Here is an example of what is more confusing: member function or overload operator. You probably can figure out what it does without explanation:

container << x1 << x2;

Yes, these are two push_back.

Or to be precise, if container is std::vector - we will need to use push_back . If std::set - we need insert. If std::map - we need operator[] or insert. If std::stack - that would be push. If x1 is container too, you will need std::copy. 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.

Of cause 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 reference manual handy.

With RO, it will stay the same as shown in above example. No IFs no ORs. RO will treats STL containers, C-strings, C-arrays and RO-Ranges in the same way and it provides uniform interface for all of them. Using operators interface actually enforces uniformity: there are not so many operators to choose from, and they have fixed number of arguments.

In next example we need to perform simple operation: remove a value from a std::vector. This is why C++ is called "too expert friendly":

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

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

And people that use above, are saying that overloaded operators are confusing:

container - x;

Above expression won’t change if we use any container (or range) or if x is not a value, but a predicate, iterator or sub-range .

One more. Given as example of terseness and simplicity of C++11 by Herb Sutter in Lambdas, Lambdas Everywhere talk:

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, syntax will be the same v / x.

Usually result of what is found in above is used in loop that process all found elements. Writing this loop and comparing found iterator to vec.end() is rather low level way of doing things.

RO provides mulch simpler to use - filtered (and lazy) range for this: vec | (_1>val). Result of operator| is range from all found elements.

One of the reasons why RO choose 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 classic:

container | sort | unique | reverse;

If RO used functions instead of operators, this would be much more complex expression with 4-level deep parenthesis. Here RO under the hood passes function pointers from <algorithm> to operator|.

RO strives to provide simpler interface and more comprehensible code. RO, similarly to STL, has zero overhead, and is as efficient as hand written for-loop.

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 to tell about SCC first.

Without SCC, minimal complete example for above erase code 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};  vec - 2;  vec'
{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 can print almost any STL objects directly. Items in print operator (and last expression which will be printed) are separated by , not by <<.

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

Print operator are _ and __. Longer one terminates output with endl. With these, equivalent to previous example would be:

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, if you wish 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);  R=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

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

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

Note that we can not define overloaded operations if arguments are only POD types (like c-string). C++ do not allow this. This is a reason why we have to wrap s with range(). For binary ops, we need to wrap at least one of operands in a class.

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

scc 'str s("abc");  (s << "XYZ") - "bc"'
aXYZ

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). Examples where return_type is the same as elem_type:

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 with arbitrary return_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 std::multiplies<T>()
6

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

scc 'vint V{1,2,3};  V || min'                  // uses 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

Filter are defined with operators|(Range, Predicate) and operator|(Range, elem_type).

Predicate is functor with signature compatible with bool(*)(elem_type). If such is used with pipe operator, all elements which evaluate to false with predicate - 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 and should be as efficient as hand written for-loop.

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{1,2,3,2};   size(V|2)'
2

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

Something interesting:

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

Why empty range? Because this is range with all elements in V equal to 2. But all 2 were replaced. Underling container V is now {1, 42, 3, 42}, and {1, 42, 3, 42} | 2 is empty range.

Evaluate: range|stl-algorithm

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

scc 'vint{3,1,2} | sort | 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{3,1,2};  sort(+V,-V);  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} | sort | unique'
{1, 2, 3}

Which is equivalent to:

scc 'vint V{3,1,2,3};  sort(+V,-V);  auto e=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

Status

RO was tested with GCC-4.8-pre-release and CLANG-3.2. It is knows that it will not work with GCC-4.7.2

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