Jonathan Boccara's blog

Dry-comparisons: A C++ Library to Shorten Redundant If Statements

Published January 3, 2020 - 0 Comments

Daily C++

Have you ever felt that the code you write in if statements doesn’t look as natural as the specification expresses them?

For example, if the specification looked like this: “if any of x, y, or z is less than 100, then perform such and such action”, a common way to express this condition in C++ is this:

if (x < 100 || y < 100 || z < 100)
{
    //...

This expression has the problem that we had to write < 100 three times. And code duplication is rarely a good thing. Also, it is expressed in the language of a computer rather than a language of humans.

A new C++ library, Dry-comparisons, authored by Björn Fahller, now allows to write this instead:

if (any_of{x,y,z} < 100)
{
    //...

This removes the duplication inside of the if statement, and makes the if statement look closer to the specification, and the language of humans. And looking closer to the specification makes if statements more expressive.

The interface of any_of is interesting, and its implementation is also instructive. Its usage of several features of modern C++ end up making it quite concise. Let’s review it in detail.

How this works

Here is an excerpt of the code of any_of, taken from the Dry-comparisons library code:

template <typename ... T>
class any_of : std::tuple<T...>
{
public:
    using std::tuple<T...>::tuple;

    //...

    template <typename U>
    constexpr bool operator<(const U& u) const {
        return std::apply([&](const auto& ... a) { return ((a < u) || ...);},
                          get());
    }

    // ...

private:
    constexpr const std::tuple<T...>& get() const { return *this;}
};

Inheriting from std::tuple

any_of is a class that inherits from std::tuple. Do we have the right to inherit from standard components? The answer is yes. Even if they don’t have a virtual destructor, like std::tuple? The answer is yes again, but there is a risk and as well as an advantage.

The risk when inheriting from a class that does not have a virtual destructor is to delete a pointer to base class that points to an object of the derived class. For instance:

auto myAnyPointer = new any_of{1, 2, 3};
std::tuple<int, int, int>* myTuplePointer = myAnyPointer;
delete myTuplePointer;

In this case delete calls the destructor of std::tuple only, and not the destructor of any_of. This leads to a partially destructed object, and undefined behaviour.

But if we look closely, the inheritance between any_of and std::tuple is private. This means that the above example would not compile. Here private inheritance is used to model the “implemented-in-terms-of” relationship. Read item 39 of Effective C++ for more about when to use private inheritance.

On the other hand, the advantage of inheriting from std::tuple is that any_of benefits from all the interface of std::tuple. Anything you can do on a tuple, you can do on an any_of. This allows in particular to reuse the constructor of std::tuple:

using std::tuple<T...>::tuple;

This in turn allows to write expressions such as:

any_of{x,y,z}

The operators

Let’s now focus on the implementation of operator<:

    template <typename U>
    constexpr bool operator<(const U& u) const {
        return std::apply([&](const auto& ... a) { return ((a < u) || ...);},
                          get());
    }

This code uses several features of modern C++. Let’s analyse them one by one:

constexpr allows the operator to be used in expression evaluated at compile time.

std::apply takes two arguments: a function, and a std::tuple. The function itself takes several arguments, and std::apply calls the function by passing it the elements of the tuple as parameters. The function in question is a template lambda, and its implementation uses fold expressions.

The arguments in the template lambda contain the keyword auto: (const auto& ... a). The ... makes it a variadic template. The equivalent in a template function would look like this:

template<typename... Ts>
auto f(const Ts&... a)

The body of the lambda: return ((a < u) || ...); is a C++17 fold expression. The beginning of the expression, (a < u), is repeated by the compiler as many times as the number of arguments in the variadic template pack. For example, with three arguments x, y, and z, this would expand as this:

(x < u || y < u || z < u)

Finally, the tuple containing the values to pass to the lambda is the any_of itself (remember, it is a tuple, as it inherits from std::tuple), returned by the get member function.

Both u (the parameter of the operator<), and *this (to call the get member function), are captured by reference by the lambda, with [&].

Quite a few modern C++ features in a couple of lines of code!

The other things Dry-comparisons lets you do

We’ve seen one use case of any_of in detail, but Dry-comparisons also features the counterparts of the other predicates on ranges of the STL: all_of and none_of. As of this writing, it doesn’t offer their Boost complement, one_of, but I suppose that it could in theory.

Also, we’ve looked at the example of operator<, but as you can imagine, the library implements the other type of comparisons as well: ==, !=, <, <=, >, >=.

What do you think of the expressions made possible by this library? Would they make your code more expressive?

You will also like

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