Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Python

C++ list comprehension

4.00/5 (1 vote)
17 Dec 2019CPOL4 min read 25.7K  
C++ list comprehension

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:

Python
[x.lower() for x in ["A","B","C"]]
// ["a", "b", "c"]
Python
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:

C++
#include <iostream>
#include <boost/phoenix.hpp>
int main()
{
    // create a callable object taking one argument (also called lazy function)
    auto mod2 = boost::phoenix::placeholders::arg1 % 2 == 1;
    // lambda equivalent
    // auto mod2 = [] (const auto& arg1) { return arg1 % 2 == 1; };
    // execute the lazy function
    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:

C++
#include <iostream>
#include <boost/phoenix.hpp>
int main()
{
    // create a callable object taking two arguments
    auto mul = boost::phoenix::placeholders::arg1 * boost::phoenix::placeholders::arg2;
    // execute the lzay function
    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:

C++
#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:

C++
#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:

C++
#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:

C++
#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);
    // 4, 3
}

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:

C++
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

Python
# Python
listOfWords = ["this","is","a","list","of","words"]
items = [ word[0] for word in listOfWords ]
C++
// C++
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

Python
# Python
[x.lower() for x in ["A","B","C"]]
C++
// C++
char e;
// make std::tolower lazy
auto ph_lower = comprehension::make_lazy<char, decltype(std::tolower), std::tolower>()
comprehension::CompVec<std::vector<char>> vec
    (
        // use lazy function ph_lower in lazy context
          ph_lower(ref(e))
        , for_each_(val(std::string("ABC")), ref(e))
    );

Example #3

Python
// Python
[i for i in range(0, 100) if i % 2 == 0]
C++
// C++
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)