Contents
Last year, I have written about constexpr
and compile time constants in general. Last week, the Draft International Standard for C++17 has been sent to its ballot. Time to provide some updates to earlier posts!
While the new standard won’t be official until much later this year, the big compilers already are implementing many C++17 features. Many of them are already available on released versions or on trunk builds of those compilers. If you want to try the new constexpr
additions without having to build your own compiler from source, have a look at pages like Compiler Explorer or Wandbox.
Constexpr lambdas
Lambdas have become constexpr
ready in C++17. That means they can be used in constexpr
contexts and the closure objects are of literal types, as long as the captured members have literal types as well.
template <typename I>
constexpr auto adder(I i) {
//use a lambda in constexpr context
return [i](auto j){ return i + j; };
}
//constexpr closure object
constexpr auto add5 = adder(5);
template <unsigned N>
class X{};
int foo() {
//use in a constant expression
X<add5(22)> x27;
//...
}
I know this will make it still harder for some to not suffer from “Overlambdification”, but what can we do – it’s already out there (call the police!)
Constexpr if
With C++17 we have the possibility to evaluate conditional expressions at compile time. The compiler is then able to eliminate the false branch completely. From a certain point of view, compilers did that already if you had an if
-statement with a condition that was a compile time constant: Compilers and optimizers were able to figure out when our code contained a glorified if (true)
and would optimize away the else
branch.
However with the old if
, the other branch still had to compile. With if constexpr
that is no longer the case, the compiler will no longer attempt that. Of course it still needs to be valid C++ syntax, since the parser at least needs to figure out where the conditional block ends.
template <class T>
auto foo(T t) {
if constexpr(std::is_same_v<T, X>) {
return t.a_function_that_exists_only_for_X();
} else {
std::cout << t << '\n';
return;
}
}
void bar() {
X x;
auto i = foo(x);
foo(23);
}
Here, the a_function_that_exists_only_for_X
method can not be called for anything but an object of type X
. The call foo(23)
would lead to a compile error had the condition been a plain old if
. In addition, you see that depending on the branch the compiler takes, the return type of the function differs. It is int
for parameters of type X
and void
for anything else.
In practice, this is very much the same as if the compiler had taken the two branches apart for us into two separate functions:
auto foo(X x) {
return x.a_function_that_exists_only_for_X();
}
template <class T>
auto foo(T t) {
std::cout << t << '\n';
}
In fact, that’s how we should have written these functions in the first place. They do unrelated things (unless the X
method is a weird print functionality) and return different things. They are two functions that are unrelated except for their very generic name. Don’t stitch together what does not belong together.
On the other hand, if constexpr
allows us to put together what does belong together but had to be kept apart until now. Many use cases of tag dispatch, enable_if
and special case overloads can be solved with if constexpr
.
I had an example in a post about variadic templates:
template <class Head, class... Tail>
void print(Head const& head, Tail const&... tail){
std::cout << head;
if constexpr(sizeof...(tail) > 0) {
std::cout << ", ";
print(tail...);
}
}
Before C++17, those functions would have to be divided into the generic one and a special case function that takes only the Head
parameter.
Constexpr fixes to the standard library
There were some features in the standard library that lacked constexpr
specifiers. These have been added in many places. Most notable are std::array
and range access functions like std::begin
and std::end
etc.
That means, std::array
is now a literal type as long as the element type is a literal type, and most of its operations can be used at compile time. Of course, the constexpr
-ness of std::begin
and the like depends on the container: Since std::vector
is not a literal type, neither vec.begin()
nor std::begin(vec)
are constexpr, but std::begin(arr)
is constexpr for both C-arrays and std::array
.
Permalink
We are using constexpr to specify static data members with complex types using
static constexpr auto blah = f_returns_complex_type();
The initializer must be constexpr too, and you have to drop from std:string to const char*, but for our use it works.
NB: gcc6.2 has a bug where the linker fails for example on the following:
struct CompilesButNotLinksUnderGcc62 {
static constexpr auto tp = make_tuple(“a”, 1);
}
This seems to be fixed in gcc 6.3.1
Permalink
The lambda and if constexpr stuff was interesting… The if constexpr was useful. Stay useful my friend. 😉