C++ 17 vs. C++ 14 — if-constexpr

We are excited to see that if-constexpr made it to C++ 17. You can try it yourself using the current trunk of clang.

In this blog-post we revisit some C++ 14 code and try to use the new feature.

Not having if-constexpr at your disposal, you often need to resort to elaborate meta-programming techniques, utilising template pattern-matching, overload-resolution and SFINAE.

Example 1 — getting the nth-arg

Many template meta-programs operate on variadic-type-lists. In C++ 14, getting the nth-type of an argument lists is often implemented the following way:

template<unsigned n>
struct Arg {
template<class X, class…Xs>
constexpr auto operator()(X x, Xs…xs) {
return Arg<n-1>{}(xs…);
}
};
template<>
struct Arg<0> {
template<class X, class…Xs>
constexpr auto operator()(X x, Xs…) {
return x;
}
};
template<unsigned n>
constexpr auto arg = Arg<n>{};
// arg<2>(0,1,2,3,4,5) == 2;

C++ 17 makes this slightly more intuitive:

template<unsigned n>
struct Get {
template<class X, class…Xs>
constexpr auto operator()(X x, Xs…xs) {
if constexpr(n > sizeof…(xs) ) {
return;
} else if constexpr(n > 0) {
return Get<n-1>{}(xs…);
} else {
return x;
}
}
};

Example 2 — API — shimming

Sometimes you want to support an alternative API. C++ 14 provides an easy way to check if an object can be used in a certain way:

template<class T>
constexpr auto supportsAPI(T x) -> decltype(x.Method1(), x.Method2(), true_type{}) {
return {};
}
constexpr auto supportsAPI(…) -> false_type {
return {};
}

Implementing custom behaviour in C++ 14 can be done like this:

template<class T>
auto compute(T x) -> decltype( enable_if_t< supportsAPI(T{}), int>{}) {
return x.Method();
}
template<class T>
auto compute(T x) -> decltype( enable_if_t<!supportsAPI(T{}), int>{}) {
return 0;
}

C++17:

template<class T>
int compute(T x) {
if constexpr( supportsAPI(T{}) ) {
// only gets compiled if the condition is true
return x.Method();
} else {
return 0;
}
}

This is very convenient as code that belongs semantically together is not scattered across multiple functions. Furthermore, you can even define lambdas containing if-constexpr.

Example 3 — Compile-time algorithm-picking

Often you need to find the best algorithm based on a set on rules and properties of a type. There are many solutions. For instance, the STL uses TypeTags to pick the right algorithm for some given iterators.

struct FooTag {};
struct BarTag {};
auto foldFF(…) {}
auto foldFB(…) {}
auto foldBF(…) {}
auto foldBB(…) {}
struct A {
/* … */
using tag = FooTag;
};
struct B {
/* … */
using tag = BarTag;
};
template<class L, class R>
auto fold(L l,R r, FooTag, BarTag) { foldFB(l,r); }
/* more dispatching functions*/
template<class L, class R>
auto fold(L l, R r) {
return fold(l,r,
typename L::tag{},
typename R::tag{} );
}

However, once you have more complex rules, you might need a more powerful solution — SFINAE:

C++ 14:

struct BazTag : FooTag, BarTag {};
template<class L, class R,
enable_if_t<
is_same<L::tag, FooTag>::value &&
is_base_of<R::tag, BarTag>::value
> fold(L l, R r) {
return foldFB(l,r);
}

With C++ 17 you can describe these rules with less boilerplate and in a clearer way:

template<class L, class R>
auto fold(L l, R r) {
using lTag = typename L::tag;
using rTag = typename R::tag;
if constexpr( is_base_of<rTag, BarTag>::value ) {
if constexpr( is_same<lTag, FooTag>::value ) {
return foldFB(l,r);
} else {
return foldBB(l,r);
} else {
return foldFF();
}
}

This is very practical as working with ifs is more intuitive than using a variety of language-features.

Refactoring meta-functions becomes as simple as ordinary code. With if-constexpr, worrying about ambiguous overloads and other unexpected complications is a thing of the past.

We will upgrade our compiler as soon as Clang 3.9 is stable.