C++23: Even more constexpr

Sandor Dargo - May 24 '23 - - Dev Community

Ever since C++ introduced the constexpr keyword in C++11, each new standard brought us more and more opportunities to make our code increasingly constexpr, in other words, compile-time execution friendly.

In this article, we are going to review briefly what changes with C++23 on this front.

Non-literal variables in constexpr functions

P2242R3 proposes to remove the restriction that a constexpr function cannot contain

  • a definition of a variable of a non-literal type
  • a definition of a variable of static or thread storage duration,
  • or a goto statement,
  • or an identifier label.

The rationale behind this change is that the presence of the listed things in a function is not a problem as long as they are not evaluated at compile-time. We should remind ourselves that a constexpr function may or may not be evaluated at compile-time.

Let's suppose that in a constexpr function we want to call a piece of code that is guaranteed to be evaluated at compile-time. Then we need to have that piece of code in a block under the condition of either if consteval or if (std::is_constant_evaluated()).

With this paper, the following code becomes valid.

template<typename T> constexpr bool f() {
  if (std::is_constant_evaluated()) {
    // ...
    return true;
  } else {
    T t; // This could have been problematic before
    // ...
    return true;
  }
}
struct nonliteral { nonliteral(); };
static_assert(f<nonliteral>());
Enter fullscreen mode Exit fullscreen mode

As nonliteral is a non-literal type, the compilation should fail without this proposal, even though the line that causes the failure is not in a constant-evaluated context.

This change is available starting from GCC 12 and Clang 15.

Relaxing some more constexpr restrictions

As I mentioned in the intro, every new standard relaxes a bit constexpr conditions and in fact, even the previous section discussed relaxations. But there is more. Thanks to Barry Revzin and P2448R2.

Let's see the two main issues the paper wants to solve.

Functions that are not yet constexpr

As I wrote earlier, a constexpr function might be evaluated at compile-time but might not. Whether or not a call can be constant-evaluated might depend on the parameters passed in. Often it seems straightforward that a function cannot be constant-evaluated because of the non-constexpr functions it calls. But never say never! More and more functions in the standard library become constexpr and conditionally marking functions constexpr - as in the next happens - is painful:

#include <optional>

#if __cpp_lib_optional >= 202106
constexpr
#endif
void h(std::optional<int>& o) {
    o.reset();
}
Enter fullscreen mode Exit fullscreen mode

Explicitly defaulted functions follow different rules

Another problem Barry wanted to address is related to explicitly defaulted functions. Let's use the following template to demonstrate the issue:

template <typename T>
struct Wrapper {
    constexpr Wrapper() = default;
    constexpr Wrapper(Wrapper const&) = default;
    constexpr Wrapper(T const& t) : t(t) { }

    constexpr T get() const { return t; }
    constexpr bool operator==(Wrapper const&) const = default;
private:
    T t;
};
Enter fullscreen mode Exit fullscreen mode

If you mark a member function constexpr, it means that it might be evaluated in a constant-evaluation context.

On the other hand, if you default a member function, it means that it must be constexpr-compatible in all instantiations. Yet, not all the compilers complain if you violate this rule and those that raise an error, they don't raise the error in each case.

In the case of a template, this means that all instantiations should be constexpr-compatible. If that's not possible, the current solution is to remove the constexpr from the explicitly defaulted constructors and operator==(). We might end up with an instantiation where they would be usable in a constant-evaluation context, yet they are not marked as constexpr

What is changing after all?

With the acceptance of this proposal, there are a couple of restrictions removed.

Constructors and destructors will follow the same rules as functions in terms of constexpr. In addition, an explicitly defaulted function on its first declaration will not only be implicitly inline but also implicitly constexpr if it satisfies the requirements of a constexpr function.

And even constexpr functions will get some relaxed rules - beyond the relaxation presented in the previous section. They can return and/or take as parameters non-literal types.

This change is available starting from GCC 13.

Permitting static constexpr variables in constexpr functions

P2647R1 corrects a hole in the standard. As the proposal says, there is no good reason why a constexpr function cannot have today a static constexpr local variable.

While this is fine:

char xdigit(int n) {
  static constexpr char digits[] = "0123456789abcdef";
  return digits[n];
}
Enter fullscreen mode Exit fullscreen mode

Its constexpr version wouldn't compile without some workarounds presented in the [paper](https://wg21.link/P2647R1:

constexpr char xdigit(int n) {
  static constexpr char digits[] = "0123456789abcdef";
  return digits[n];
}
Enter fullscreen mode Exit fullscreen mode

Not anymore! The above code is becoming legit. GCC 13 and Clang 16 already support it!

constexpr type_info::operator==()

At the moment, typeid is allowed in constant expressions, but the returned std::type_info object has no constexpr methods, therefore it's not practically usable in a constexpr context.

P1328R1 makes the equality operator (operator==()) constexpr to std::type_info becomes practically usable in compile-time functions!

It's fascinating how far the constexpr came. I mean, typeid() and std::type_info are used for Run-Time Type Information (RTTI) and yet we talk about constexpr...

GCC 12, Clang 17 and MSVC 19.33 already provide this feature!

constexpr for <cmath> and <cstdlib>

Until now, <cmath> and <cstdlib> has barely contained any constexpr functions. P0533R9 aims to improve on this situation in order to facilitate compile-time programming. These headers have been simply neglected so far, otherwise, there is no reason why std::chrono::abs is constexpr but std::abs is not.

In (8.E-G sections of P0533R9 you can find the full list of functions that are going to become constexpr. Luckily it's quite a long list!

This is yet to be implemented by the compilers!

constexpr std::unique_ptr

P2273R3 is bringing us constexpr unique pointers. The requirements of constexpr were loosened by P0784R7, adopted by C++20. With that new and delete might be used in certain constexpr-contexts, so it was worth trying making std::unique_ptr also constexpr. It worked out fine and it's already available in the new versions of major compilers.

The authors also tried to make shared_ptr constexpr, but due to missing compile-time atomic support it's not possible for the moment to implement shared_ptr according to the standard. The authors are going to explore their possibilities further.

GCC 12, Clang 16 and MSVC 19.33 already provide this feature!

constexpr for integral overloads of std::to_chars() and std::from_chars()

At compile-time, there is currently no standard way to make conversions between numbers and strings. Now that a std::string can be instantiated at compile-time, we are much closer to get a constexpr std::format and this is a gap.

std::to_chars and std::from_chars are fundamental blocks for parsing and formatting as they are locale-independent, they don't throw and don't allocate memory. Except for the floating point overloads now they will become constexpr thanks to P2291R3.

They will be another step forward to have constexpr std::format.

GCC 13, Clang 16 and MSVC 19.34 already provide this feature!

constexpr std::bitset

P2417R2 extends the constexpr interface of std::bitset. So far, only one of the constructors and operator[] was marked as constexpr. However, since std::string can be constexpr, all the internals - and therefore the full API - of std::bitset can be constexpr.

GCC 13, Clang 16 and MSVC 19.34 already provide this feature!

DR: constexpr for std::optional and std::variant

P2231R1 adds some missing constexpr both to std::optional and std::variant. As the already referenced P0784R7 made it possible to use new and delete at compile-time (by using std::construct_at), there was no reason to not making optional and variant fully constexpr.

GCC 11, Clang 13 and MSVC 19.31 already provide this fix!

Conclusion

In this article, we reviewed the constexpr related changes in C++23. There are quite many of them, and our options for compile-time programming are growing! You can check on C++ Reference whether they are implemented by your compiler of choice.

Connect deeper

If you liked this article, please

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .