In this post, you will find a templated class and optional small macro that allows to write list comprehensions à la python in C++.
Introduction
This is a small snippet for one line modification of a container mostly like for_each
but encapsulates the resulting container allowing for random access regardless of the type of container.
Background
I was recently watching a video that described Python's list comprehensions and I immediately wanted to know if C++ could offer the same functionality with roughly the same amount of syntax.
In Python, if you want to apply a transformation to all or some elements of a list, you can write something like this:
nums = [1,2,3,4]
result = [x*x for x in nums]
I spent some time browsing the C++ standard algorithms library and briefly Googling the web and I couldn't find (though it might exist) something that was as simple in syntax as Python's.
So I wrote this little class to that end and thought of sharing it just to prove that if C++ doesn't provide a feature (exactly as you want it) out of the box, there is a very good chance the language will allow you to implement it yourself.
Using the Code
In order to make use of the auto
keyword and template deduction, you'll need at least C++17.
I created two classes in
and into
(the class into
to use with std::array
at compile time) and two macros each
and only
to cheat my way into being able to write something like the Python example above. If not using exactly the same syntax, at least with the same (or better) level of verbosity of what's being done to the list:
set nums = {1,2,3,4};
auto result = in(nums, each(x, x*x));
auto result = in(nums, each(x, (x>2)?x*x:x));
auto result = in(nums, only(x, x<2, x*x));
auto result = in(nums, only(x, x<4 && cout<<x, x*x));
You can put these classes and the macros in one header of your choice (I called mine comprehension.h). I'd advice you to include it only in cpp files as the macros are by their very essence intrusive and you probably don't want them being around in places where you don't need this feature.
#include <array>
template <typename T, size_t S>
class into : public std::array<T,S>
{
public:
into (std::array<T,S>& input, auto func)
{
for (size_t i=0; i<this->size(); i++)
{
(*this)[i] = func(input[i]);
}
}
into (std::array<T,S>& input, auto condition, auto func)
{
for (size_t i=0; i<this->size(); i++)
{
if (!condition(input[i])) continue;
(*this)[i] = func(input[i]);
}
}
};
template<typename T, template<typename...> class C, typename... Args>
class in
{
public:
in (C<T, Args...>& input, auto condition, auto func)
{
for (auto inp=input.begin(); inp!=input.end(); inp++)
{
if (!condition(*inp)) continue;
(void)items.insert(items.end(),func(*inp));
}
}
in (C<T, Args...>& input, auto func)
{
for (auto inp=input.begin(); inp!=input.end(); inp++)
{
(void)items.insert(items.end(),func(*inp));
}
}
auto begin () const { return items.begin(); }
auto end () const { return items.end(); }
auto size () const { return items.size(); }
operator auto () const { return items; }
const auto& operator [] (const size_t& i) const
{
auto iter = items.begin();
std::advance(iter,i);
return *iter;
}
private:
C<T, Args...> items;
};
#define only(x,c,f) [](const auto& x) {return bool(c);}, [](const auto& x) {return f;}
#define each(x,f) [](const auto& x) {return f;}
And the code to test it:
#include <vector>
#include <deque>
#include <map>
#include <set>
#include <list>
#include <string>
using namespace std;
int main()
{
switch(6)
{
case 1:
{
array nums = {1,2,3,4};
auto result = into(nums, only(x, x<3, x*x));
return result[1];
}
case 2:
{
string letters = "hello";
auto result = in(letters, each(x, toupper(x)));
return result[1];
}
case 3:
{
list nums = {1,2,3,4};
auto result = in(nums, each(x, (x>2)?x*x:x));
return result[1];
}
case 4:
{
set nums = {1,2,3,4};
auto result = in(nums, only(x, x<3, x*x));
return result.size();
}
case 5:
{
deque nums = {1,2,3,4};
auto result = in(nums, each(x, x*x));
return result[1];
}
case 6:
{
vector nums = {1,2,3,4};
auto result = in(nums, each(x, (true)?x*x:x));
return result[1];
}
case 7:
{
map<int,string> nums = { {1,"one"}, {2,"two"}, {3,"three"}, {4,"four"} };
auto result = in(nums, each(x, make_pair(x.first*x.first,x.second)));
return result[1].first;
}
}
return 0;
}
Points of Interest
You could achieve something similar just by having a function like this:
template<typename T, template<typename...> class C, typename... Args>
C<T, Args...> in (C<T, Args...>& input, auto func)
{
C<T, Args...> items;
for (auto inp=input.begin(); inp!=input.end(); inp++)
(void)items.insert(items.end(),func(*inp));
return items;
}
But using (and extending) the classes in
and into
, you can simplify the way you access almost all the standard containers.
One thing that could be added is a way to specify a range, it could be tricky to make it work for containers that have no random access, but I think it can be done.
History
- 10th August, 2020: Initial version