5
\$\begingroup\$

I wrote a small wrapper function to choose between runtime (std) sqrt and a constexpr version, as a continuation of the vector class I published here earlier:

C++ mathematical n-dimensional vector template using fold expressions

It seems to work fine and Compiler Explorer shows that the sqrt function is completely compiled out.

The relevant code (sqrt coming from the standard library):

constexpr T cx_sqrt(const T& m) const {
    int i = 0;
    while((i*i) <= m) {
        i++;
    }
    i--;
    T d = m - (i * i);
    T p = d / (2 * i);
    T a = i + p;
    return a - (p * p) / (2 * a);
}

constexpr T choose_sqrt(const T& m) const {
    return (__builtin_constant_p(m) && __builtin_constant_p(*m)) ?  cx_sqrt(m) : sqrt(m);
}

template <size_t... I>
constexpr T magnitude_impl(std::index_sequence<I...>) const {
    return choose_sqrt(((values[I] * values[I]) + ...));
}

Any gotchas using a method like this?

\$\endgroup\$
6
  • \$\begingroup\$ Do you need a particular compiler for __builtin_constant_p()? Any special flags or #include headers? \$\endgroup\$ Aug 11, 2017 at 11:59
  • 1
    \$\begingroup\$ GCC provides it as an extension, so does Clang (at least 6, don't know how far back). I've come across some webpages claiming this won't work for O0, but since it's not my normal use case I haven't tested it yet. Other than that the compiler needs to support constexpr, of course. Now you've made me think of it more, it would be relatively easy to check if the built-in is available and provide the fallback instead. \$\endgroup\$
    – Stefan
    Aug 11, 2017 at 12:22
  • \$\begingroup\$ Do you mean something different by "needs to support constexpr" that's not already implied by the C++17 tag? I guess there are different grades of "support", but if the compiler doesn't do any compile-time evaluation, you'll automatically get the fallback, won't you? BTW, good question, and the only reason I'm not answering is that I don't see anything I can improve! \$\endgroup\$ Aug 11, 2017 at 12:39
  • \$\begingroup\$ @Stefan: Where does values come from in magnitude_impl? \$\endgroup\$
    – einpoklum
    Aug 11, 2017 at 19:01
  • \$\begingroup\$ @einpoklum, its probably a member function of a class. There is a const specifier at the end of declaration. \$\endgroup\$ Aug 12, 2017 at 9:55

3 Answers 3

4
\$\begingroup\$

As of C++20, you can use std::is_constant_evaluated(), which is more portable and considerably easier to read. I realize you have only tagged C++17, but I just wanted to point out that there is now a cleaner solution to this problem.

#include <type_traits>
#include <cmath>

namespace example
{
    namespace impl
    {
        template<typename T>
        constexpr auto sqrt(const T& m) noexcept -> decltype(std::sqrt(m)) {
            // implementation details
        }
    }

    template<typename T>
    constexpr auto sqrt(const T& m) noexcept {
        if (std::is_constant_evaluated())
            return impl::sqrt(m);
        else
            return std::sqrt(m);
    }
}
\$\endgroup\$
2
\$\begingroup\$

What do you think would happen if you tried to compile cx_sqrt<uint64_t>(~0)?

Be kind to your compiler, do something like this for the constexpr case:

namespace detail {

template <typename T>
constexpr T sqrt_helper(T x, T lo, T hi)
{
    if (lo == hi) { return lo; }

    const T mid = (lo + hi + 1) / 2;

    if (x / mid < mid)
        return sqrt_helper<T>(x, lo, mid - 1);
    else
        return sqrt_helper(x, mid, hi);
}

} // namespace detail

template <typename T>
constexpr T cx_sqrt(T& x)
{
    return detail::sqrt_helper<T>(x, 0, x / 2 + 1);
}
\$\endgroup\$
2
\$\begingroup\$

Based on Toby Speight's answer I went ahead and converted this to a generic template / utility function. The added checks provide a fallback to the runtime function if the required features aren't present.

The updated utility function:

// These are Clang specifics, but any compiler supporting them will use them instead of the basic checks.
#ifndef __has_builtin
#define __has_builtin(x) 0
#endif
#ifndef __has_feature
#define __has_feature(x) 0
#endif

/**
 * Take two functions `ct` and `rt` and run `ct(args...)` if compile time execution is possible,
 * `rt(args...)` if not.
 * @param  ct   Compile time run function.
 * @param  rt   Run time run function.
 * @param  args Arguments to pass to one of above functions.
 * @return      Return value of either `ct` or `rt`.
 */
template <typename R, typename... Args>
constexpr R ct_rt(R (*const ct)(Args...), R (*const rt)(Args...), Args ...args) {
// We need `constexpr` support for this to make any sense.
#if (__has_builtin(__builtin_constant_p) && __has_feature(__cpp_constexpr)) || \
    (defined(__GNUC_PATCHLEVEL__) && __cplusplus >= 201103L)
    return ((__builtin_constant_p(args) && __builtin_constant_p(*args)) && ...)
        ? ct(args...)
        : rt(args...);
#else
    // Fall back to the runtime version, instead of running the constexpr version at runtime.
    return rt(args...);
#endif
}

Use case:

float slow_and_accurate(const float n) { ... }
float fast_and_sloppy(const float n) { ... }

result = ct_rt(slow_and_accurate, fast_and_sloppy, 3.14f);
\$\endgroup\$
1
  • \$\begingroup\$ I think it was a mistake to design constexpr so as to be intentionally hidden from detection, rather than being able to overload based on it. So how do I do that on Microsoft compiler or otherwise portibly? \$\endgroup\$
    – JDługosz
    Apr 27, 2018 at 21:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.