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.