Search code examples
c++constexprfallback

How can I have a fallback run-time implementation in a constexpr function


Context

I'm making a math library and I need to use sqrt().
As sqrt() is not a constexpr function I've implemented a constexpr version of sqrt() and a faster that std::sqrt() that uses assembly, so must be used on run-time.
Now, I'm using all this to calculate the length of a vector, and this function can be constexpr because I'm making constexpr everything possible in the struct.

constexpr inline Real length() const { return const_sqrt<Real>(lengthSquared());}

This will work, if the lenght() function is called in a non constexpr context, it simply run in run-time, but I have a faster run-time implementation of sqrt() than const_sqrt().

Question

How can I switch to use one implementation or another based on if the function is executed on compile-time or run-time.
Something like this:

constexpr inline Real length() const { 
    return IN_RUN_TIME 
         ? fast_sqrt<Real>(lengthSquared()) 
         : const_sqrt<Real>(lengthSquared());
}

Solution

  • The only standard-compliant solution is to use std::is_constant_evaluated, as suggested by cigien:

    constexpr inline Real length() const
    { 
        return std::is_constant_evaluated()
             ? fast_sqrt<Real>(lengthSquared()) 
             : const_sqrt<Real>(lengthSquared());
    }
    

    The problem with this approach is that std::is_constant_evaluated() will only return true if the return value of length() is used to initialize a constexpr variable, or is otherwise required to be constexpr.

    This is suboptimal in situations where the value of lengthSquared() is known at compile-time (thus const_sqrt could be used), but the return value of length() is not required to be constexpr. Then is_constant_evaluated will return false, so fast_sqrt is going to be used instead, unnecessarily postponing1 the computation to runtime.

    The workaround for that is to use a non-standard GCC built-in (also supported by Clang): __builtin_constant_p. Unlike std::is_constant_evaluated, it has an 'expression' parameter and checks if the value of the expression is known at compile-time (which might depend on optimization settings).

    I suggest that __builtin_constant_p should be used if it's available, falling back to std::is_constant_evaluated otherwise. (And if you're using a pre-C++20 compiler, this built-in is your only option.)

    #ifdef __GNUC__ // Defined by GCC and Clang
    #define KNOWN_AT_COMPILE_TIME(...) __builtin_constant_p(__VA_ARGS__)
    #else
    #define KNOWN_AT_COMPILE_TIME(...) std::is_constant_evaluated()
    #endif
    
    constexpr inline Real length() const
    { 
        return KNOWN_AT_COMPILE_TIME(lengthSquared())
             ? fast_sqrt<Real>(lengthSquared()) 
             : const_sqrt<Real>(lengthSquared());
    }
    

    1 I assume fast_sqrt doesn't work at compile-time. Otherwise there's no point in having a separate const_sqrt.