Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detecting constexpr with SFINAE

I'm working on upgrading some C++ code to take advantage of the new functionality in C++11. I have a trait class with a few functions returning fundamental types which would most of the time, but not always, return a constant expression. I would like to do different things based on whether the function is constexpr or not. I came up with the following approach:

template<typename Trait> struct test {     template<int Value = Trait::f()>     static std::true_type do_call(int){ return std::true_type(); }      static std::false_type do_call(...){ return std::false_type(); }      static bool call(){ return do_call(0); } };  struct trait {     static int f(){ return 15; } };  struct ctrait {     static constexpr int f(){ return 20; } };  int main() {    std::cout << "regular: " << test<trait>::call() << std::endl;    std::cout << "constexpr: " << test<ctrait>::call() << std::endl; } 

The extra int/... parameter is there so that if both functions are available after SFINAE, the first one gets chosen by overloading resolution.

Compiling and running this with Clang 3.2 shows:

regular: 0 constexpr: 1 

So this appears to work, but I would like to know if the code is legal C++11. Specially since it's my understanding that the rules for SFINAE have changed.

like image 487
K-ballo Avatar asked Mar 05 '13 19:03

K-ballo


People also ask

How do I know if a function is constexpr?

The easiest way to check whether a function (e.g., foo ) is constexpr is to assign its return value to a constexpr as below: constexpr auto i = foo(); if the returned value is not constexpr compilation will fail.

Is constexpr guaranteed?

Quick A: constexpr guarantees compile-time evaluation is possible if operating on a compile-time value, and that compile-time evaluation will happen if a compile-time result is needed.

Is constexpr always evaluated at compile time?

constexpr functions will be evaluated at compile time when all its arguments are constant expressions and the result is used in a constant expression as well.

Should I use constexpr everywhere?

Yes. I believe putting such const ness is always a good practice wherever you can. For example in your class if a given method is not modifying any member then you always tend to put a const keyword in the end.


2 Answers

NOTE: I opened a question here about whether OPs code is actually valid. My rewritten example below will work in any case.


but I would like to know if the code is legal C++11

It is, although the default template argument may be considered a bit unusual. I personally like the following style better, which is similar to how you (read: I) write a trait to check for a function's existence, just using a non-type template parameter and leaving out the decltype:

#include <type_traits>  namespace detail{ template<int> struct sfinae_true : std::true_type{}; template<class T> sfinae_true<(T::f(), 0)> check(int); template<class> std::false_type check(...); } // detail::  template<class T> struct has_constexpr_f : decltype(detail::check<T>(0)){}; 

Live example.


Explanation time~

Your original code works because a default template argument's point of instantiation is the point of instantiation of its function template, meaning, in your case, in main, so it can't be substituted earlier than that.

§14.6.4.1 [temp.point] p2

If a function template [...] is called in a way which uses the definition of a default argument of that function template [...], the point of instantiation of the default argument is the point of instantiation of the function template [...].

After that, it's just usual SFINAE rules.


† Atleast I think so, it's not entirely clear in the standard.

like image 170
Xeo Avatar answered Sep 19 '22 21:09

Xeo


Prompted by @marshall-clow, I put together a somewhat more-generic version of an type-trait for detecting constexpr. I modelled it on std::invoke_result, but because constexpr depends on the inputs, the template arguments are for the values passed in, rather than the types.

It's somewhat limited, as the template args can only be a limited set of types, and they're all const when they get to the method call. You can easily test a constexpr wrapper method if you need other types, or non-const lvalues for a reference parameter.

So somewhat more of an exercise and demonstration than actually-useful code.

And the use of template<auto F, auto... Args> makes it C++17-only, needing gcc 7 or clang 4. MSVC 14.10.25017 can't compile it.

namespace constexpr_traits {  namespace detail {  // Call the provided method with the provided args. // This gives us a non-type template parameter for void-returning F. // This wouldn't be needed if "auto = F(Args...)" was a valid template // parameter for void-returning F. template<auto F, auto... Args> constexpr void* constexpr_caller() {     F(Args...);     return nullptr; }  // Takes a parameter with elipsis conversion, so will never be selected // when another viable overload is present template<auto F, auto... Args> constexpr bool is_constexpr(...) { return false; }  // Fails substitution if constexpr_caller<F, Args...>() can't be // called in constexpr context template<auto F, auto... Args, auto = constexpr_caller<F, Args...>()> constexpr bool is_constexpr(int) { return true; }  }  template<auto F, auto... Args> struct invoke_constexpr : std::bool_constant<detail::is_constexpr<F, Args...>(0)> {};  } 

Live demo with use-cases on wandbox

like image 26
TBBle Avatar answered Sep 19 '22 21:09

TBBle