3

Suppose I have a configuration function to be defined by the user of the library which may or may not be constexpr.

constexpr int iterations() { return 10; }
// Or maybe:
int iterations() { return std::atoi(std::getenv("ITERATIONS")); }

// I'm okay with requiring the constexpr version to be:
constexpr auto iterations() { return std::integral_constant<int, 10>{}; }

Now I have a function that has different behavior depending on the value of iterations(), but this function should be constexpr if iterations is, because I want the user to be able to use it in constexpr if they configured the library to allow for it:

/*constexpr*/ std::uint32_t algorithm(std::uint32_t x) {
    auto const iterations = ::iterations();

    // For illustration purposes
    for (int i = 0; i < iterations; ++i) {
        x *= x;
    }

    return x;
}

What can I do to algorithm to make the function constexpr if iterations() is? In short, I want something like constexpr(constexpr(::iterations())).


Note that "conditional constexpr" is usually dependent on a template parameter as in Conditionally constexpr member function in which case the constexpr keyword can just be used, but in this case, I want the constexpr to be conditional on something which is not a template parameter, but a statically known function. Marking algorithm as constexpr is a compilation error:

error: call to non-'constexpr' function 'bool iterations()'
max66
  • 65,235
  • 10
  • 71
  • 111
Justin
  • 24,288
  • 12
  • 92
  • 142

3 Answers3

4

You can silence the compiler diagnostics by ensuring that there is some set of template parameters and function arguments for which a call to your function is a constant expression even if ::iterations() is not a constant expression. For example:

template <bool True = true>
constexpr std::uint32_t algorithm(std::uint32_t x) {
    if constexpr (True) {
        auto const iterations = ::iterations();

        for (int i = 0; i < iterations; ++i) {
            x *= x;
        }

        return x;
    } else {
        return 0;
    }
}

algorithm<false>(meow) is a constant expression whenever meow is, so a compiler cannot complain (https://godbolt.org/z/GvE9ME).

Casey
  • 41,449
  • 7
  • 95
  • 125
  • This sort of makes a mockery out of the rule though, no? Kind of like, no you can't do `static_assert(false);` but yes you can do `static_assert(always_false_v);` – Barry Feb 26 '21 at 20:23
  • @Barry Yes it does. It would be nice if there were a more explicit way to opt out of some of the "helpful" checks the language provides for templates, but absent that, we have gnarly workounds. `template struct S { template > // ...` is the classic case - which we fixed in the core language with requires-clauses - but certainly not the only case. – Casey Mar 01 '21 at 19:29
2

Taking inspiration from your answer, you can use concepts to impose that iteration() return a integral constant.

The important part is remember that algorithm() ha to be a template function or concept (requires) can't works, so you can impose an unused and defaulted template parameter

//VVVVVVVVVVVVVVVVVVVVVVVVVV <-- add this to activate `requires`  
  template <typename = void>
  constexpr std::uint32_t algorithm(std::uint32_t x)
          requires is_integral_constant<decltype(::iterations())>
   { return ::algorithm_impl(x, ::iterations()); }

The following is your example simplified

template <typename T>
constexpr bool is_integral_constant = false;

template <typename T, T value>
constexpr bool is_integral_constant<std::integral_constant<T, value>> = true;

constexpr std::uint32_t algorithm_impl(std::uint32_t x, int iterations) {
    for (int i = 0; i < iterations; ++i) {
        x *= x;
    }

    return x;
}

template <typename = void>
constexpr std::uint32_t algorithm(std::uint32_t x)
        requires is_integral_constant<decltype(::iterations())>
 { return ::algorithm_impl(x, ::iterations()); }

std::uint32_t algorithm(std::uint32_t x) {
    return ::algorithm_impl(x, ::iterations());
}
max66
  • 65,235
  • 10
  • 71
  • 111
  • I'm fairly certain that you can use `requires` on non-templates (you certainly can for a non-template inside a templated class) and that GCC's complaint is just that their concepts implementation is incomplete. Regardless, this is an excellent way to get around the issues of the compiler instantiating the body of `algorithm` against the constraint. – Justin Feb 26 '21 at 18:53
  • 1
    @Justin - According [this page](https://en.cppreference.com/w/cpp/language/constraints), "Class templates, function templates, and non-template functions (typically members of class templates) may be associated with a constraint, which specifies the requirements on template arguments", so (if I understand correctly) "template arguments" are required. – max66 Feb 26 '21 at 19:06
  • 1
    @Justin - The surreal aspect is that this solution works also if the template parameter isn't involved. – max66 Feb 26 '21 at 19:12
0

I'm okay with requiring the constexpr version to be: ... std::integral_constant<int, 10>

Because you are okay with requiring the constexpr version of the function to have a different return type, we can detect this special type and condition the constexpr on that by using constraints (C++20's requires). Note that we have to further wrap the body in if constexpr because the compiler still checks the body of the function:

template <typename T>
constexpr bool is_integral_constant = false;

template <typename T, T value>
constexpr bool is_integral_constant<std::integral_constant<T, value>> = true;

constexpr std::uint32_t algorithm_impl(std::uint32_t x, int iterations) {
    for (int i = 0; i < iterations; ++i) {
        x *= x;
    }

    return x;
}

constexpr std::uint32_t algorithm(std::uint32_t x)
        requires is_integral_constant<decltype(::iterations())> {
    if constexpr (is_integral_constant<decltype(::iterations())>) {
        return ::algorithm_impl(x, ::iterations());
    } else {
        // Unreachable, but enough to convince the compiler that there is a
        // constexpr friendly path through the function
        return 0xDEADBEEF;
    }
}

std::uint32_t algorithm(std::uint32_t x) {
    return ::algorithm_impl(x, ::iterations());
}

Demo

Justin
  • 24,288
  • 12
  • 92
  • 142
  • Question: if you impose `requires is_integral_constant`, why you check `if constexpr (is_integral_constant)` inside the body? Isn't ever true? – max66 Feb 26 '21 at 18:13
  • I mean: what's wrong with simply `constexpr std::uint32_t algorithm(std::uint32_t x) requires is_integral_constant { return ::algorithm_impl(x, ::iterations()); }` ? – max66 Feb 26 '21 at 18:16
  • @max66 Yes, it is always true, but without the check and just having `return :;algorithm_impl(x, ::iterations())`, the compiler complains that the `constexpr` function is never constexpr (the only path calls the non-constexpr `::iterations()`) – Justin Feb 26 '21 at 18:17
  • This is the part I don't understand: "the only path calls the non-constexpr `::iterations()`". Given (`require`) that `decltype(::iterations())` is an integral constant, how can be "non-constexpr"? – max66 Feb 26 '21 at 18:20
  • @max66 See https://godbolt.org/z/ooY8bW . I don't know what in the standard makes this happen, but it does (it could even be a bug in clang's concepts implementation) – Justin Feb 26 '21 at 18:24
  • Added an answer to explain where the problem is. – max66 Feb 26 '21 at 18:48