Walletfox.com

How to use lambda expression to sort a vector of custom objects (since C++11)

In this article, we are going to sort a vector of user-defined objects, specifically Skyscrapers, with the help of a lambda expression introduced by the C++11. In the first variant of the problem, we are going to sort the objects according to their height. In the second variant, we are going to sort them according to two different attributes, namely, the height and the number of floors.

Note: Sorting user-defined objects in C++20 is really neat, check out the C++20 version of this article here.

Lambda expressions

The sorting algorithm needs a rule according to which to sort the custom objects. Our implementation will use lambda expressions for this purpose. A lambda expression is actually a syntactic shortcut for a function object, i.e. an object that can be called as if it was a function (to that end, it defines the operator() ). A lambda expression bypasses the explicit creation of a function object and that is the reason why it is often used to encapsulate shortcodes passed to algorithms. The basic syntax of a lambda expression goes as follows:

[captures] (argument list) -> return-type 
{ 
       lambda body; 
}

The argument list resembles an argument list of an ordinary function and it is optional. In our case, the argument list consists of two references to Skyscraper objects to compare, i.e. (Skyscraper const& s1, Skyscraper const& s2). These will be used by our sorting algorithm to decide ordering of objects.

[] (Skyscraper const& s1, Skyscraper const& s2) -> bool
{
       return s1.height() < s2.height();
};

The return type is optional and resembles a return type of an ordinary function. It can be omitted if the lambda body contains just one return statement or the expression does not return a value. In our case, the return type is bool. We may choose to include it in our code, but the code would work just fine without it.

[] (Skyscraper const& s1, Skyscraper const& s2) -> bool 
{
       return s1.height() < s2.height();
};

The body of a lambda expression contains just what an ordinary function would contain. It can refer to arguments, locally declared variables, global variables, class data members if declared within a class and captured variables (more on that soon). In our case, the body contains the details about how the comparison is performed and the return statement.

[] (Skyscraper const& s1, Skyscraper const& s2) -> bool
{
       return s1.height() < s2.height();
};

The captures are the variables that a lambda expression can obtain (capture) from the surrounding scope. Our capture list will remain empty for the Variant 1 of the problem, however, for the Variant 2 of the problem the capture list will store the information about the attribute according to which we should perform our sorting.

[] (Skyscraper const& s1, Skyscraper const& s2) -> bool
{
       return s1.height() < s2.height();
};
[property](Skyscraper const& s1, Skyscraper const& s2) -> bool 
{
       switch(property) {
             case Property::HEIGHT: 
               return s1.height() < s2.height();
               break;       
             case Property::FLOORS: 
               return s1.nrFloors() < s2.nrFloors();
               break;
             default:
               return s1.height() < s2.height();
               break;
       }
};

Sorting Skyscrapers - Variant 1

Below, you can see the class Skyscraper with its attributes name and height. We are going to use this class to demonstrate sorting according to a single parameter, namely height.

class Skyscraper
{
public:
    Skyscraper(std::string const& s, double h) : m_name(s), m_height(h){}
    auto name() const {return m_name;}
    auto height() const {return m_height;}
    void print() const {
            std::cout << this->m_name << " " 
                      << this->m_height << '\n'; }

private:
    std::string m_name;
    double m_height;
};

Let's look at the main below. We initialize our vector of four Skyscraper objects and define a lambda expression sortRuleLambda with references to two Skyscraper objects passed as arguments, bool as a return type and no captures. The body of the lambda expression performs the comparison and returns the result. The sortRuleLamda is passed as the third argument to the sorting algorithm.

int main()
{
    auto skyscrapers = std::vector<Skyscraper> {
        Skyscraper ("Empire State", 381),
        Skyscraper ("Petronas", 452),
        Skyscraper ("Burj Khalifa", 828),
        Skyscraper ("Taipei", 509)
    };

    auto sortRuleLambda = [] (Skyscraper const& s1, Skyscraper const& s2) -> bool
    {
       return s1.height() < s2.height();
    };
    
    std::sort(skyscrapers.begin(), skyscrapers.end(), sortRuleLambda);

    for (auto const& s : skyscrapers)
        s.print();

}

The program outputs the following result:

Empire State 381
Petronas 452
Taipei 509
Burj Khalifa 828

How to sort a vector of objects according to a specific attribute - use of a capture

To show you how to sort objects according to a specific attribute, we introduce an extra attribute into the Skyscraper class - namely the number of floors. Now we have different attributes according to which we can sort and we somehow need to inform the sorting algorithm about the desired attribute.

class Skyscraper
{
public:
    Skyscraper(std::string const& s, double h, int nrFloors) : 
                    m_name(s), m_height(h), m_nrFloors(nrFloors){}
    auto name() const {return m_name;}
    auto height() const {return m_height;}
    auto nrFloors() const {return m_nrFloors;}
    void print() const {
            std::cout << this->m_name << " " 
                      << this->m_height << " " 
                      << this->m_nrFloors << '\n'; }

private:
    std::string m_name;
    double m_height;
    int m_nrFloors;
};

In order to inform the sorting function about the attribute according to which it should sort our Skyscraper objects, we need to introduce an additional variable named 'property'. The 'property' can assume several values, namely, FLOORS or HEIGHT embodied as an enum class. How do we pass this information to our lambda expression? We pass this information in the [captures] of the lambda expression. In this way, we inform the lambda expression about the attribute according to which it should perform the sorting.

enum class Property {HEIGHT, FLOORS};

int main()
{
    auto skyscrapers = std::vector<Skyscraper> {
        Skyscraper ("Empire State", 381, 102),
        Skyscraper ("Petronas", 452, 88),
        Skyscraper ("Burj Khalifa", 828, 163),
        Skyscraper ("Taipei", 509, 101)
    };

    auto property = Property::FLOORS;
    auto sortRuleLambda = [property] (Skyscraper const& s1, Skyscraper const& s2) -> bool
    {
       switch(property) {
             case Property::HEIGHT: 
               return s1.height() < s2.height();
               break;       
             case Property::FLOORS: 
               return s1.nrFloors() < s2.nrFloors();
               break;
           default:
               return s1.height() < s2.height();
               break;
       }
    };

    std::sort(skyscrapers.begin(), skyscrapers.end(), sortRuleLambda);

    for (auto const& s : skyscrapers)
        s.print();

}

If you run the code, you will get the following output (the second number represents the number of floors).

Petronas 452 88
Taipei 509 101
Empire State 381 102
Burj Khalifa 828 163

As an exercise, try changing the variable to HEIGHT.

Note: At the beginning we mentioned that a lambda expression is a syntactic shortcut for a function object which overloads the operator(). Below you can see, how an equivalent function object looks like. The body of the lambda expression is equivalent to the body of the overloaded operator(). The arguments of the lambda expression (Skyscraper const& s1, Skyscraper const& s2) are equivalent to the arguments of the overloaded operator(). Finally, the variable 'property' which was the capture of our lambda expression becomes an attribute of the function object. An instance of such a function object would be passed as a third argument to the sorting algorithm.

enum class Property {HEIGHT, FLOORS};

struct EntityComp {
  Property property;
  EntityComp(Property property) {this->property = property;}
  bool operator()(Skyscraper const& s1, Skyscraper const& s2) const {
      switch(property) {
             case Property::HEIGHT: 
               return s1.height() < s2.height();
               break;       
             case Property::FLOORS: 
               return s1.nrFloors() < s2.nrFloors();
               break;
             default:
               return s1.height() < s2.height();
               break;
       }
  }
};
std::sort ( skyscrapers.begin(), skyscrapers.end(), EntityComp(FLOORS) );

Tagged: C++