Projects home
https://github.com/xR3b0rn/cpp_list_comprehension
Introduction
The library introduces Pythons list comprehension to C++ based on boost::phoenix
.
Background
Pythons list comprhension
List comprehension basically means to creating lists out of lists. Python therefore provides some nice syntax. At this point I only want provide some small code snipets here so you get a basic feeling of how list comprehension in Python work. If you want to learn more about Pythons list comprehensions, look into the official documentation of Python.
Here are the examples:
[x.lower() for x in ["A","B","C"]]
// ["a", "b", "c"]
listOfWords = ["this","is","a","list","of","words"]
[ word[0] for word in listOfWords ]
// ["t", "i", "a", "l", "o", "w"]
boost::phoenix
First at all I have to introduce some basics about boost::phonix
so you can get a better idea of what the library is doing.
boost::phoenix functions
boost::phoenix
encapsulates functions in callable objects which can be invoked later, so called lazy functions. With this boost::phoenix
provides something like an alternative to lambdas. To bascially understand what lazy functions are, take a look at this example:
#include <iostream>
#include <boost/phoenix.hpp>
int main()
{
auto mod2 = boost::phoenix::placeholders::arg1 % 2 == 1;
std::cout << mod2(5) << std::endl;
}
As you can see, beside the existing lambda syntax, boost::phoenix
provides an even more pretty syntax for doing the same thing.
You may wonder how boost::phoenix
is doing this, but understanding this isn't that hard. Taking the example again, you can think of boost::phoenix::placeholders::arg1
as an object with the %
-operator defined, in which the operator is returning a callable object which has defined the ==
-operator. The ==
-operator then returns a new callable object taking one argument. Calling the callable function then executes as first the %
-operation and as then the ==
-operationon on the provided int
values and return it's bool
result.
Another example of lazy functions is this piece of code:
#include <iostream>
#include <boost/phoenix.hpp>
int main()
{
auto mul = boost::phoenix::placeholders::arg1 * boost::phoenix::placeholders::arg2;
std::cout << mul(2, 2) << std::endl;
}
The *
-operator in the example is taking boost::phoenix::placeholders::arg1
and boost::phoenix::placeholders::arg2
as argument and return a callable object taking two arguments. The callable object then can be invoked and the multiplication is executed.
boost::phoenix
also provides some basic predefined functions for STL constainers you can use like this:
#include <vector>
#include <iostream>
#include <boost/phoenix.hpp>
int main()
{
std::vector<int> vec;
auto push = push_back(ref(vec), arg1);
push(5);
}
Note that's important here to use boost::phoenix::ref
since elsewise the std::vector
-instance would be captured by value. Spoken in lambda the difference between using ref
and not using ref
means choosing between [vec](auto& arg1) { vec.push_back(arg1); }
and [&vec](auto& arg1) { vec.push_back(arg1); }
.
So far so good, but what happens, when you want use your own functions in lazy context?
If you want use your own function in a lazy context e.g. ::sqrt
from cmath
, you can't simple write ::sqrt(boost::phoenix::placeholders::arg1)
. That is because ::sqrt
isn't a lazy function (there is no function ::sqrt
taking a boost::phoenix::placeholders::arg1
-object as argument). Therefore we have to define our own lazy ::sqrt
-function type.
boost::phoenix
provides therefor boost::phoenix::function
which is taking a callable struct with the return type provided as template argument:
#include <cmath>
#include <boost/phoenix.hpp>
struct lazy_sqrt
{
using result_type = float;
result_type operator()(float f) const
{
return ::sqrt(f);
}
};
boost::phoenix::funciton<lazy_sqrt> ph_sqrt;
For this the cpp_list_comprehension library provides a function which is simplifying the creation of lazy functions and summarizes the steps needed to create a lazy function, so it's possible to do it that way:
#include <make_lazy.h>
auto ph_sqrt = comprehension::make_lazy<float, float(float), &::sqrt>();
Now ph_sqrt
can be used in lazy context like we used push_back
before.
Note: The comprehension::make_lazy
-function only works for none member functions. If you want use member function, you have to do it the boost::phoenix
-way. For more information about defining your own stuff look at the official documentation of boost::phoenix
.
boost::phoenix statements
boost::phoenix
also provides functionallity of lazy statements for some basic statemens like the for_
, while_
and if_
-statement:
#include <iostream>
#include <boost/phoenix.hpp>
int main()
{
using namespace boost::phoenix;
using placeholders::arg1;
int i = 5;
auto stmt = while_(ref(i)-- > arg1);
stmt[std::cout << ref(i) << std::endl](2);
}
The syntax looks the same for while_
and for_
, the only exception is do_
. For some reason the do_
statement is breaking the consistence of the syntax, so this statement is useless for us. If you want to use this nevertheless the cpp_list_comprehension library is providing it's own implementation of the do_
statement called do__
. And since boost::phoenix
doesn't provide anything to achieve for_each_
the cpp_list_comprehension also provides a implementation for this.
This small crash course should be enought to understand the basics of boost::phoenix
and to use the cpp_list_comprehension library. To learn more about boost::phoenix
go to the offical documentation: https://www.boost.org/doc/libs/1_64_0/libs/phoenix/doc/html/index.html
cpp_list_comprehension
The cpp_list_comprehension library is using boost::phoenix
to provide list comprehension syntax similar to the Pythons list comprehension.
For this, the library introduces a new data type comprehension::CompVec<T>
. This type inherites from the template parameter passed as argument and pulls all the constructors in scope. So writing comprehension::CompVec<std::vector<int>> a{1, 2, 3, 4};
will create a comprehension::CompVec<std::vector<int>>
object with the values 1, 2, 3, 4 stored in it.
To get Python like list comprehension the comprehension::CompVec
-type is defining a new constructor:
class CompVec
{
template <class F1, class... Args, std::enable_if_t<std::is_invocable_v<F1>, void*> = nullptr>
CompVec(const F1& fu, const Args&... args);
}
The constructor is taking a boost::phoenix
lazy function as first parameter and an arbitrarily amount of boost::phoenix
lazy statements.
Note: To help the compiler to choose the correct constructor, the CompVec
's constructor is only available if the first argument passed to the constructor is callable. I think this should be ok, but may be this could lead to errors in special cases, e.g. when storing callable objects in the vector. I didn't make any investigations in that direction.
Using the code
Example #1
listOfWords = ["this","is","a","list","of","words"]
items = [ word[0] for word in listOfWords ]
std::string str;
comprehension::CompVec<std::vector<char>> vec
(
ref(str)[val(0)]
, for_each_(val(std::vector<std::string>{"this","is","a","list","of","words"}), ref(str))
);
Example #2
[x.lower() for x in ["A","B","C"]]
char e;
auto ph_lower = comprehension::make_lazy<char, decltype(std::tolower), std::tolower>()
comprehension::CompVec<std::vector<char>> vec
(
ph_lower(ref(e))
, for_each_(val(std::string("ABC")), ref(e))
);
Example #3
// Python
[i for i in range(0, 100) if i % 2 == 0]
int i;
comprehension::CompVec<std::vector<int>> l2
(
ref(i)
, for_(ref(i) = 0, ref(i) <= 100, ref(i)++)
, if_(ref(i) % 2 == 0)
);
Points of Interest
History