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?
3 Answers 3
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);
}
}
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);
}
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);
-
\$\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– JDługosz2018年04月27日 21:39:26 +00:00Commented Apr 27, 2018 at 21:39
Explore related questions
See similar questions with these tags.
__builtin_constant_p()
? Any special flags or#include
headers? \$\endgroup\$values
come from inmagnitude_impl
? \$\endgroup\$const
specifier at the end of declaration. \$\endgroup\$