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

Multi-Container List Comprehension in C++

4.00/5 (1 vote)
10 Aug 2020MIT2 min read 6.8K  
List comprehensions in C++
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:

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

C++
set nums = {1,2,3,4};

/* insert all elements multiplied by themselves */
auto result = in(nums, each(x, x*x));         

/* insert all elements but only multiply those that fulfill the condition */
auto result = in(nums, each(x, (x>2)?x*x:x)); 

/* insert only the elements that fulfill the condition */
auto result = in(nums, only(x, x<2, x*x));   

/* insert and print only the elements that fulfill the condition */
auto result = in(nums, only(x, x<4 && cout<<x, x*x)); /* prints: 123 */

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.

C++
#include <array>

// For using only with std::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]);
      }
    }
};

// To work with vector, deque, set, map, list and string with the exact same syntax
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:

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

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

License

This article, along with any associated source code and files, is licensed under The MIT License