Overview of Expression
Expressions are essentially functors, with an ability to compose itself with other expressions. You start with a simple expression, and then you go on composing more complex expressions using simpler ones. It can be best understood by examples. So here you go.
//a function which takes a functor as argument
//and then invoke it passing 0 to 9, and print the result.
template<typename Functor>
void f(Functor && fun)
{
for ( int i = 0 ; i < 10 ; i++ )
std::cout << fun(i) << std::endl; //invoke fun for each i in [0, 10)
}
expression<int> x; //x is a simple expression (identity expression)
f(x); //print 0 to 9
auto y = x * x - 4 * x + 4; //compose an expression y using x and literals
f(y); //print value of (i * i - 4 * i + 4) for each i in [0, 10)
f( (x + 10)* y ); //x and y are expressions. compose further using them
//and pass the resulting expression to f which
//prints value of [(i + 10) * (i * i - 4 * i + 4)] for each i in [0, 10)
Comparison of expression with lambda
Since expressions are functors, they can be used in place of lambdas, because they simplify the code. Consider this code which uses lambda,
std::vector<int> v {0,1,2,3,4,5};
auto it = std::find_if(begin(v), end(v), [](int i) { return i == 2; } );
Using expresson the above std::find_if
can be written as,
expression<int> x; //declare an expression variable first
auto it = std::find_if(begin(v), end(v), x == 2 );
which is much more concise. Well that is not where you would use expression (or lambda for that matter), because you would use std::find
as:
auto it = std::find(begin(v), end(v), 2 ); //simplest!
That is better expressed without expression! But consider this code which uses expression:
auto it = std::find_if(begin(v), end(v), (x*x+4) == (4*x) ); //find x such that LHS == RHS
Now compare it with the lambda version,
auto it = std::find_if(begin(v), end(v), [](int i ) { return (i*i+4) == (4*i); } );
The advantage of expression over lambda is that once you declare x
, you can reuse it and that too in many different ways. Few more examples,
if ( std::any_of(begin(v), end(v), x < 0 ) )
{
std::cout << "v contains at least one negative number." << std::endl;
}
int n = std::count_if ( begin(v), end(v), x % 2 == 0 ); //count even numbers
std::cout << "v contains " << n < " even numbers." << std::endl;
As we see, expressions make the code concise without destroying its readability. On the contrary, it increases the readability.
Member expression
C++11 has std::mem_fn which creates wrappers for member-function-pointers and member-data-pointers. The following example illustrates its usage:
struct person
{
int age;
std::string name;
int get_age() const { return age; }
};
std::vector<person> ps = get_persons();
auto age = std::mem_fn(&person::age); //creates a wrapper for person::age
std::vector<int> ages;
std::transform(ps.begin(),
ps.end(),
std::back_inserter(ages),
age); //for each element e in ps, get `e.age` and add it to vector `ages`.
for(auto && x : ages)
std::cout << x << std::endl; //print each element of ages
The problem is that the wrapper age
, created by std::mem_fn
, doesn’t support composition. We cannot use it to do this, for example:
//pseudocode : it would not compile
std::transform(ps.begin(),
ps.end(),
std::back_inserter(ages),
10 * age ); //add 10 times of age to the vector `ages`.
To accomplish this, composition library provides a function called memexp
which is just like std::mem_fn
, except that it supports composition. So with it we can do this:
auto age = memexp(&person::age); //creates a composable wrapper!
std::transform(ps.begin(),
ps.end(),
std::back_inserter(ages),
10 * age ); //okay now
Or we may use member function of person
instead, as:
auto age = memexp(&person::get_age); //notice the difference here!
std::transform(ps.begin(),
ps.end(),
std::back_inserter(ages),
10 * age ); //same as before!
We can use it in filtering as:
std::vector<person> filtered;
std::copy_if(ps.begin(),
ps.end(),
std::back_inserter(filtered),
age >= 13 && age <= 19 ); //filter teenagers
which is equivalent to this lambda version:
std::vector<person> filtered;
std::copy_if(ps.begin(),
ps.end(),
std::back_inserter(filtered),
[](person const & p) { return p.age >= 13 && p.age <= 19 } );
Apart from being concise, the expression version is definitely more readable. Even more, memexp allows us to pipe as shown below (using _m
which is a wrapper object of memexp
):
//first get the name of person object,
//which then gets passed to the next pipe
//which returns the size of the name.
auto namesize = _m(&person::name) | _m(&std::string::size); //we can also use memexp instead.
std::vector<std::size_t> sizes;
std::transform(ps.begin(),
ps.end(),
std::back_inserter(sizes),
namesize); //note: namesize is composable, so we can pass (namesize * 10) instead!
which is same as (lambda version):
std::vector<std::size_t> sizes;
std::transform(ps.begin(),
ps.end(),
std::back_inserter(sizes),
[](person const & p) { return p.name.size(); } );