Table of Contents

Before C++17, we had a few quite ugly-looking ways to write static if (if that works at compile time). For example, you could use tag dispatching or SFINAE. Fortunately, that’s changed, and we can now benefit from if constexpr and Concepts!

Let’s see how we can use it and replace some std::enable_if code.

Updated for C++20 in April 2021.

Intro

Compile-time if in the form of if constexpr is a fantastic feature that went into C++17. With this functionality, we can improve the readability of some heavily templated code.

Additionally, with C++20, we got Concepts! This is another step to have almost “natural” compile-time code.

This blog post was inspired by an article @Meeting C++ with a similar title. I’ve found three additional examples that can illustrate this new feature:

  • Number comparisons
  • (New!) Computing average on a container
  • Factories with a variable number of arguments

But for a start, I’d like to recall the basic knowledge about enable_if to set some background.

Why compile-time if?

At first, you may ask why we need static/constexpr if and complex templated expressions? Wouldn’t “normal” if just work?

Here’s a test code:

template <typename T>
std::string str(T t) {
    if (std::is_convertible_v<T, std::string>)
        return t;
    else
        return std::to_string(t);
}

The above routine might be some simple utility that is used to print stuff. As to_string doesn’t accept std::string we can test and return t if it’s already a string.

Sound simple… but try to compile this code:

// code that calls our function
auto t = str("10"s);

You might get something like this:

In instantiation of 'std::__cxx11::string str(T) [with T = 
std::__cxx11::basic_string<char>; std::__cxx11::string =
 std::__cxx11::basic_string<char>]':
required from here
error: no matching function for call to 
'to_string(std::__cxx11::basic_string<char>&)'
    return std::to_string(t);

is_convertible yields true for the type we used (std::string), and we can just return t without any conversion… so what’s wrong?

Here’s the main point:

The compiler compiled both branches and found an error in the else case. It couldn’t reject the “invalid” code for this particular template instantiation.

That’s why we need static if that would “discard” code and compile only the matching statement. To be precise, we’d like to have a syntax check for the whole code, but some parts of the routine would not be instantiated.

std::enable_if

One way to write static if in C++1114 is to use enable_if.

enable_if (and enable_if_v since C++14). It has quite strange syntax:

template< bool B, class T = void >  
struct enable_if;

enable_if will evaluate to T if the input condition B is true. Otherwise, it’s SFINAE, and a particular function overload is removed from the overload set. This means that on false the compiler “rejects” the code - this is precisely what we need.

We can rewrite our basic example to:

template <typename T>
enable_if_t<is_convertible_v<T, string>, string> strOld(T t) {
    return t;
}

template <typename T>
enable_if_t<!is_convertible_v<T, string>, string> strOld(T t) {
    return to_string(t);
}
// std:: prefix ommited

Not easy… right?

See below how we can simplify such code with if constexpr from C++17. After you read the post, you’ll be able to rewrite our str utility quickly.

Use Case 1 - Comparing Numbers

First, let’s start with a simple example: close_enough function that works on two numbers. If the numbers are not floating points (like when we have two ints), we can compare them directly. Otherwise, for floating points, it’s better to use some abs < epsilon checks.

I’ve found this sample from at Practical Modern C++ Teaser - a fantastic walkthrough of modern C++ features by Patrice Roy. He was also very kind and allowed me to include this example.

C++1114 version:

template <class T> constexpr T absolute(T arg) {
   return arg < 0 ? -arg : arg;
}

template <class T> 
constexpr enable_if_t<is_floating_point<T>::value, bool> 
close_enough(T a, T b) {
   return absolute(a - b) < static_cast<T>(0.000001);
}
template <class T>
constexpr enable_if_t<!is_floating_point<T>::value, bool> 
close_enough(T a, T b) {
   return a == b;
}

As you see, there’s a use of enable_if. It’s very similar to our str function. The code tests if the type of input numbers is is_floating_point. Then, the compiler can remove one function from the overload resolution set.

And now, let’s look at the C++17 version:

template <class T> constexpr T absolute(T arg) {
   return arg < 0 ? -arg : arg;
}

template <class T>
constexpr auto precision_threshold = T(0.000001);

template <class T> constexpr bool close_enough(T a, T b) {
   if constexpr (is_floating_point_v<T>) // << !!
      return absolute(a - b) < precision_threshold<T>;
   else
      return a == b;
}

Wow… so just one function that looks almost like a normal function.

With nearly “normal” if :)

if constexpr evaluates constexpr expression at compile time and then discards the code in one of the branches.

But it’s essential to observe that the discarded code has to have the correct syntax. The compiler will do the basic syntax scan, but then it will skip this part of the function in the template instantiation phase.

That’s why the following code generates a compiler error:

template <class T> constexpr bool close_enough(T a, T b) {
   if constexpr (is_floating_point_v<T>) 
      return absolute(a - b) < precision_threshold<T>;
   else
      return aaaa == bxxxx; // compiler error - syntax!
}

close_enough(10.04f, 20.f);

Checkpoint: Can you see some other C++17 features that were used here?

You can play with the code @Compiler Explorer

Adding Concepts in C++20

But wait… it’s 2021, so why not add some concepts? :)

Up to C++20, we could consider template parameters to be something like a void* in a regular function. If you wanted to restrict such a parameter, you had to use various techniques discussed in this article. But with Concepts, we get a natural way to restrict those parameters.

Have a look:

template <typename T>
requires std::is_floating_point_v<T>
constexpr bool close_enough20(T a, T b) {
   return absolute(a - b) < precision_threshold<T>;
}
constexpr bool close_enough20(auto a, auto b) {
   return a == b;
}

As you can see, the C++20 version switched to two functions. Now, the code is much more readable than with enable_if. With concepts, we can easily write our requirements for the template parameters:

requires std::is_floating_point_v<T>

is_floating_point_v is a type-trait (available in <type_traits> library) and as you can see the requires clause evaluates boolean constant expressions.

The second function uses new generalized function syntax, where we can omit the template<> section and write:

constexpr bool close_enough20(auto a, auto b) { }

Such syntax comes from generic lambdas. This is not a direct translation of our C++1114 code as it corresponds to the following signature:

template <typename T, typename U>
constexpr bool close_enough20(T a, U b) { }

Additionally, C++20 offers terse syntax for concepts thanks to constrained auto:

constexpr bool close_enough20(std::floating_point auto a,
                              std::floating_point auto b) {
   return absolute(a - b) < precision_threshold<std::common_type_t<decltype(a), decltype(b)>>;
}
constexpr bool close_enough20(std::integral auto a, std::integral auto b) {
   return a == b;
}

Alternatively we can also put the name of the concept instead of a typename and without the requires clause:

template <std::is_floating_point T>
constexpr bool close_enough20(T a, T b) {
   return absolute(a - b) < precision_threshold<T)>;
}

In this case, we also switched from is_floating_point_v into a concept floating_point defined in the <concepts> header.

See the code here: @Compiler Explorer

Ok, how about another use case?

Use case 2 - computing the average

Let’s stay in some “numeric” area, and now we’d like to write a function that takes a vector of numbers and returns an average.

Here’s a basic use case:

std::vector ints { 1, 2, 3, 4, 5};
std::cout << Average(ints) << '\n';

Out function has to:

  • Take floating-point numbers or integral types.
  • It returns double.

In C++20, we can use ranges for such purposes, but let’s treat this function as our playground and test case to learn.

Here’s a possible version with Concepts:

template <typename T> 
requires std::is_integral_v<T> || std::is_floating_point_v<T>
constexpr double Average(const std::vector<T>& vec) {
    const double sum = std::accumulate(vec.begin(), vec.end(), 0.0);        
    return sum / static_cast<double>(vec.size());
}

For the implementation, we need to restrict the template parameter to be integral or floating-point.

We don’t have a predefined concept that combines floating point and integral types, so we can try writing our own:

template <typename T> 
concept numeric = std::is_integral_v<T> || std::is_floating_point_v<T>;

And use it:

template <typename T> 
requires numeric<T>
constexpr double Average2(std::vector<T> const &vec) {
    const double sum = std::accumulate(vec.begin(), vec.end(), 0.0);        
    return sum / static_cast<double>(vec.size());
}

Or we can also make it super short:

constexpr double Average3(std::vector<numeric auto> const &vec) {
    const double sum = std::accumulate(vec.begin(), vec.end(), 0.0);        
    return sum / static_cast<double>(vec.size());
}

We can also rewrite it with C++14 enable_if

template <typename T> 
std::enable_if_t<std::is_integral_v<T> || std::is_floating_point_v<T>, double>
Average4(std::vector<T> const &vec) {
    const double sum = std::accumulate(vec.begin(), vec.end(), 0.0);        
    return sum / static_cast<double>(vec.size());
}

See the working code @Compiler Explorer

Use case 3 - a factory with variable arguments

In the item 18 of Effective Modern C++ Scott Meyers described a function called makeInvestment:

template<typename... Ts> 
std::unique_ptr<Investment> 
makeInvestment(Ts&&... params);

This is a factory method that creates derived classes of Investment and the main advantage is that it supports a variable number of arguments!

For example, here are the proposed types:

class Investment {
public:
    virtual ~Investment() { }

    virtual void calcRisk() = 0;
};

class Stock : public Investment {
public:
    explicit Stock(const std::string&) { }

    void calcRisk() override { }
};

class Bond : public Investment {
public:
    explicit Bond(const std::string&, const std::string&, int) { }

    void calcRisk() override { }
};

class RealEstate : public Investment {
public:
    explicit RealEstate(const std::string&, double, int) { }

    void calcRisk() override { }
};

The code from the book was too idealistic and didn’t work - it worked until all your classes have the same number and types of input parameters:

Scott Meyers: Modification History and Errata List for Effective Modern C++:

The makeInvestment interface is unrealistic because it implies that all derived object types can be created from the same types of arguments. This is especially apparent in the sample implementation code, where our arguments are perfect-forwarded to all derived class constructors.

For example if you had a constructor that needed two arguments and one constructor with three arguments, then the code might not compile:

// pseudo code:
Bond(int, int, int) { }
Stock(double, double) { }
make(args...)
{
  if (bond)
     new Bond(args...);
  else if (stock)
     new Stock(args...)
}

Now, if you write make(bond, 1, 2, 3) - then the else statement won’t compile - as there no Stock(1, 2, 3) available! To work, we need something like static if that will work at compile-time and reject parts of the code that don’t match a condition.

Some posts ago, with the help of one reader, we came up with a working solution (you can read more in Nice C++ Factory Implementation 2).

Here’s the code that could work:

template <typename... Ts> 
unique_ptr<Investment> 
makeInvestment(const string &name, Ts&&... params)
{
    unique_ptr<Investment> pInv;

    if (name == "Stock")
        pInv = constructArgs<Stock, Ts...>(forward<Ts>(params)...);
    else if (name == "Bond")
        pInv = constructArgs<Bond, Ts...>(forward<Ts>(params)...);
    else if (name == "RealEstate")
        pInv = constructArgs<RealEstate, Ts...>(forward<Ts>(params)...);

    // call additional methods to init pInv...

    return pInv;
}

As you can see, the “magic” happens inside constructArgs function.

The main idea is to return unique_ptr<Type> when Type is constructible from a given set of attributes and nullptr when it’s not.

Before C++17

In my previous solution (pre C++17) we used std::enable_if and it looked like that:

// before C++17
template <typename Concrete, typename... Ts>
enable_if_t<is_constructible<Concrete, Ts...>::value, unique_ptr<Concrete>>
constructArgsOld(Ts&&... params)
{
    return std::make_unique<Concrete>(forward<Ts>(params)...);
}

template <typename Concrete, typename... Ts>
enable_if_t<!is_constructible<Concrete, Ts...>::value, unique_ptr<Concrete> >
constructArgsOld(...)
{
    return nullptr;
}

std::is_constructible see cppreference.com - allows us to quickly test if a list of arguments could be used to create a given type.

In C++17 there’s a helper:

is_constructible_v = is_constructible<T, Args...>::value;

So we could make the code shorter a bit…

Still, using enable_if looks ugly and complicated. How about a C++17 version?

With if constexpr

Here’s the updated version:

template <typename Concrete, typename... Ts>
unique_ptr<Concrete> constructArgs(Ts&&... params)
{  
  if constexpr (is_constructible_v<Concrete, Ts...>)
      return make_unique<Concrete>(forward<Ts>(params)...);
   else
       return nullptr;
}

Super short!

We can even extend it with a little logging features, using fold expression:

template <typename Concrete, typename... Ts>
std::unique_ptr<Concrete> constructArgs(Ts&&... params)
{ 
    cout << __func__ << ": ";
    // fold expression:
    ((cout << params << ", "), ...);
    cout << "\n";

    if constexpr (std::is_constructible_v<Concrete, Ts...>)
        return make_unique<Concrete>(forward<Ts>(params)...);
    else
       return nullptr;
}

Cool… right? :)

All the complicated syntax of enable_if went away; we don’t even need a function overload for the else case. We can now wrap expressive code in just one function.

if constexpr evaluates the condition, and only one block will be compiled. In our case, if a type is constructible from a given set of attributes, then we’ll compile the make_unique call. If not, then nullptr is returned (and make_unique is not even instantiated).

C++20

With concepts we can easily replace enable_if:

// C++20:
template <typename Concrete, typename... Ts>
requires std::is_constructible_v<Concrete, Ts...>
std::unique_ptr<Concrete> constructArgs20(Ts&&... params) {
    return std::make_unique<Concrete>(std::forward<Ts>(params)...);
}

template <typename Concrete, typename... Ts>
std::unique_ptr<Concrete> constructArgs20(...) {
    return nullptr;
}

But I wonder if that’s better? I think in this case, if constexpr looks much better and easier to follow.

Here’s the working code @Compiler Explorer

Wrap up

Compile-time if is an amazing feature that significantly simplifies templated code. What’s more, it’s much expressive and nicer than previous solutions: tag dispatching or enable_if (SFINAE). Now, you can easily express your intends similarly to the “run-time” code.

We also revised this code and examples to work with C++20! As you can see, thanks to concepts, the code is even more readable, and you can “naturally” express requirements for your types. You also gain few syntax shortcuts and several ways to express such restrictions.

In this article, we’ve touched only basic expressions, and as always, I encourage you to play more with this new feature and explore.

And going back to our str example:

Can you now rewrite str function using if constexpr? :)

You can find more examples and use cases in my C++17 Book: C++17 in Detail @Leanpub or @Amazon in Print