Jonathan Boccara's blog

std::less and its Modern Evolutions

Published October 29, 2019 - 0 Comments

Daily C++

Since C++98, the C++ standard library has provided std::less, a little component that concisely expresses that you want to use operator< to perform comparisons.

std::less is a template class, conceptually equivalent to this:

template<typename T>
struct less
{
    bool operator()(T const& lhs, T const& rhs)
    {
        return lhs < rhs;
    }
};

Let’s see how std::less, as well as its siblings std::greater, std::equal, std::less_equal and std::greater_equal can be useful, and the feature of C++14 that simplified their usage: std::less<>.

A funny way of saying <

If you hadn’t encountered std::less yet, it might look like a very complicated way of comparing two values. For example, to check if a number a is smaller than another number b by using std::less, we would write something like this:

std::less<int>{}(a, b);

Instead of:

a < b;

This has the priceless advantage of… no really, the second option is better. This is not how std::less is intended to be used.

std::less comes in handy when you need to pass operator< to a function, because C++ doesn’t allow to pass operators.

To illustrate, consider the following function that takes in a function compare to compare values:

template<typename Comparator>
void f(int a, int b, Comparator compare)
{
    if (compare(a, b))
    {
        std::cout << "Hello\n";
    }
}

If you’d like it to compare values using operator<, you can’t just write this:

f(42, 43, <); // not valid C++
f(42, 43, operator<); // not valid either
f(42, 43, operator<<int, int>); // more and more complicated and still not valid

C++ doesn’t allow to pass operators. But you can pass a function object that calls an operators, such as std::less:

f(42, 43, std::less<int>{});

There is one case where this is particularly useful: when f is an algorithm, like an STL algorithm, or something that looks like an STL algorithm.

Custom comparisons in algorithms

Many algorithms perform comparisons between elements of the collections they operate on. For example, std::sort compares values two by two to determine which one goes before the other.

To perform those comparisons, STL algorithms have a default way of doing it, such as operator< or operator== (depending on whether they use equality or equivalence).

The default comparison is fine is most cases, but in some cases you want to specify another type of comparison. For example, if you have a collection of X with each one containing a Key, and you want to sort the elements according to their Keys. You can’t just call:

auto xs = std::vector<X>{x1, x2, x3, x4, x5};

std::sort(begin(xs), end(xs));

Indeed, the above code will try to call operator< on X during the sort, whereas you want to call operator< on the keys inside of each X. To achieve this, std::sort offers an overload accepting a comparator:

auto xs = std::vector<X>{x1, x2, x3, x4, x5};

std::sort(begin(xs), end(xs), [](X const& x1, X const& x2){ return x1.key() < x2.key(); });

If you implement your own algorithm, then you also want to offer that possibility, in order to follow the conventions of the STL.

To do this, you start by implementing the general case, with the custom comparator:

template<typename Iterator, typename Comparator>
Iterator myAwesomeAlgorithm(Iterator first, Iterator last, Comparator compare)
{
    // ...
}

Then you can just create a second overload that calls the first one and passes it… std::less! Or std::equal, depending on what should be your default comparison (again, equality or equivalence):

template<typename Iterator>
Iterator myAwesomeAlgorithm(Iterator first, Iterator last)
{
    return myAwesomeAlgorithm(first, last, std::less<typename Iterator::value_type>{});
}

However, using std::less forces us to write out the type of the elements to be compared: typename Iterator::value_type. This is what gets simplified in C++14.

C++14 and std::less<>{}

In C++14, you can just replace the above code by this:

template<typename Iterator>
Iterator myAwesomeAlgorithm(Iterator first, Iterator last)
{
    return myAwesomeAlgorithm(first, last, std::less<>{});
}

This looks a lot nicer. But by what magic does that work?

C++14 introduced a total specialization of the class template std::less: with std::less<void>. Note that this is not an issue for backward compatibility, because since we can’t compare void (nor even take references to it), no one used std::less<void> anyway.

std::less<void> is defined (essentially) as follows:

template<>
struct less<void>
{
    template<typename T>
    bool operator()(T const& lhs, T const& rhs)
    {
        return lhs < rhs;
    }
};

(In reality there is more code in std::less because of special cases it handles, but the main bit is that).

It looks a lot like the generic code of std::less we considered earlier, which was this:

template<typename T>
struct less
{
    bool operator()(T const& lhs, T const& rhs)
    {
        return lhs < rhs;
    }
};

Except it is the operator() that is a template, and not the class itself. The big difference it makes is that we can create an std::less<void> without passing it any template parameter, and it is the call to operator() that deduces T, just like a call to a any template function tries to deduce its template type from its arguments.

We could use std::less<void> instead of typing out all the template type:

template<typename Iterator>
Iterator myAwesomeAlgorithm(Iterator first, Iterator last)
{
    return myAwesomeAlgorithm(first, last, std::less<void>{});
}

But std::less<void> looks weird. So C++14’s std::less also make the class template parameter of std::less default to void:

template<typename T = void>
struct less
{
    bool operator()(T const& lhs, T const& rhs)
    {
        return lhs < rhs;
    }
};

This is what allows to omit the type passed to std::less:

template<typename Iterator>
Iterator myAwesomeAlgorithm(Iterator first, Iterator last)
{
    return myAwesomeAlgorithm(first, last, std::less<>{});
}

C++17 and std::less{}

C++17 allows to simplify what’s left of std::less, by not passing any template parameters at all.

Indeed, with template type deduction of constructor arguments, the compiler can figure out that when writing std::less{} what you mean is std::less<void>{}.

Let the compiler do the dirty work

Even if the technology used by std::less<void> existed since C++98 (template type deduction in class methods), this new addition is consistent with the direction of the language: offloading type deduction to the compiler.

This is what other features of Modern C++ also allow, such as auto and template type deduction for constructors.

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin