Archive for August, 2022

constexpr Function Parameters

Monday, August 29th, 2022

The Set-up

In C++, this doesn’t work:

consteval auto square(int x) -> int { return x * x; }

constexpr auto twice_square(int x) -> int { return square(x); }

The compiler complains, quite rightly:

error: call to consteval function 'square' is not a constant expression

note: function parameter 'x' with unknown value cannot be used in a constant expression

Despite the fact that twice_square is a constexpr function, its parameter x is not constexpr. We can’t use it in a static_assert, we can’t pass it to a template, we can’t call an immediate (consteval) function with it.

So far, so well known. This is the current situation in C++, although this area is a potential future expansion of constexpr (see P1045).

The Hook

And yet… we all know that C++ has some dark corners.

A disclaimer is in order. I don’t know why the technique I’m about to cover works, and I haven’t found the part of the standard that guarantees it (or not). So the possibilities lie on a spectrum:

  1. This is a valid technique, and it’s explicitly called out in the standard
    somewhere.
  2. This is a valid technique that’s a consequence of a valid reading of perhaps several parts of the standard, but not explicitly called out.
  3. This is known to the standard and left up to implementations, who happen to mostly make the same choice.
  4. This is outside the standard, but a logical consequence of how C++ must be implemented, so implementations are bound to make the same choice.
  5. This is behaviour that is prohibited by the standard.
  6. This is behaviour for which the standard imposes no requirements.

Things in buckets 1, 2 and 3 are well known. Stateful template metaprogramming is an example of something in bucket 4, that the committee would like to move into bucket 5. And bucket 6 is of course undefined behaviour.

My gut feeling is that we are somewhere in 2-3 territory here. The technique I’m about to highlight is accepted by Clang, GCC and MSVC, and the basics work in every standard since C++11. So 5 seems unlikely. But I could well be wrong — caveat lector. And let’s get to it.

The Tale

The most exciting phrase to hear in science, the one that heralds new discoveries, is not “Eureka” but “That’s funny…”

Isaac Asimov (1920–1992)

My suspicions were first aroused while reading a code review recently. The engineer who’d written the code at the time perhaps didn’t appreciate the implication of what they’d written.

template <auto F>
concept C = true;

auto do_something(auto fn) {
  static_assert(C<fn()>);
}

do_something([]{});

The actual code is removed to highlight the structure. We’re passing a lambda expression into a function template. And then we’re verifying that the result of calling the lambda satisfies a concept. And by the way, if it’s new to you that concepts can work on NTTPs, that’s also a thing — although it’s not well supported by so-called “terse syntax”.

But hold on here, fn is a function parameter. And function parameters aren’t constexpr! So why is the static_assert well-formed?

Since C++17, the function call operators of lambda expressions are implicitly constexpr. But they aren’t static (yet — see P1169), so there is an implicit object parameter here that should not be a constant expression – except that the compiler thinks that it is? Again, I’m not sure quite yet what is happening here.

A bit of experimentation shows that this works for lambdas that don’t capture. And for empty structs with function call operators. And for any kind of derived structure, as long as it is empty, including overload sets deriving from several lambda expressions in the familiar way.

template <typename... Ts> 
struct overloaded : Ts... { 
  using Ts::operator()...;
};

And on the MSVC compiler with support for P0847, it also works with explicit object parameters.

So to achieve “constexpr function parameters” it seems wrapping values inside non-capturing lambda expressions and then calling to unwrap in a constexpr context is possible. And in fact, this is the technique used by Jason Turner in C++ Weekly Episode 313, “The constexpr problem that took me 5 years to fix!”

The Wire

That on its own is strange. But then I had another idea.

#define constexpr_value(X) \
[] { \
  struct { \
    consteval operator decltype(X)() const noexcept { \
      return X; \
    } \
    using constexpr_value_t = void; \
  } val; \
  return val; \
}()

What if I wrap up a value inside an empty structure with a compile-time conversion operator? The immediately-invoked lambda expression on the outside here is just to turn the whole thing into an expression. And the alias declaration is doing basically the same job as is_transparent in the standard library: it allows us to detect compile-time values with a concept.

template <typename T, typename U>
concept compile_time = 
  requires { typename T::constexpr_value_t; } 
  and std::convertible_to<T, U>;

Now I can write the following:

consteval auto square(int x) -> int { return x * x; }

constexpr auto twice_square(compile_time<int> auto x) -> int {
  return square(x);
}

and call it:

twice_square(constexpr_value(4));

And everything is happy. In fact, twice_square doesn’t even have to be a constexpr function: the entire “constexpr-ness” is contained within constexpr_value. And the result of constexpr_value doesn’t even need to be assigned to a constexpr variable; this works just the same saying:

auto x = constexpr_value(4);
twice_square(x);

Regular non-constexpr functions can take arguments like this as use them in arbitrary constexpr contexts as needed, e.g.

auto sqrt(compile_time<double> x) -> double {
  static_assert(x >= 0, "negative numbers not allowed");
  return std::sqrt(x);
}

When I call this with a cromulent constexpr_value, all the compile-time machinery melts away and it’s a regular call to std::sqrt. When I call it with a negative constexpr_value, the static_assert fires. Contrast this with a throw inside an if consteval block or something similar that we’d typically use in a constexpr function today to signal an error at compile time: I think this is clearer and gives a nicer error.

The Shut-out

This works on all 3 major compilers, and the fundamentals work all the way back to C++11. I have noticed that the compilers differ slightly when using a constexpr_value as a NTTP, seemingly dependent on where the conversion happens. For example there is difference between:

template <auto N>
constexpr int var = N;

and

template <int N>
constexpr auto var = N;

Other than this, the 3 compilers seem remarkably in agreement about how this works.

The Sting

So what does this mean? If we have a compile-time value, and we wrap it in constexpr_value, we have potentially the following situation:

The variable declaration is not marked constexpr. The function parameter isn’t constexpr. The function itself isn’t marked constexpr, and it’s not an immediate function. But arbitrarily we can use the value in a constexpr context. We can call into a constexpr or immediate function. We can use the value in a static_assert. We can use the value as a NTTP. And we don’t have to jump through hoops with less-friendly ways to signal errors in constexpr contexts.

I’m not sure of all the potential uses yet, but this is a curious thing.