简体   繁体   中英

Compile-time or runtime detection within a constexpr function

I was excited when constexpr was introduced in C++11, but I unfortunately made optimistic assumptions about its usefulness. I assumed that we could use constexpr anywhere to catch literal compile-time constants or any constexpr result of a literal compile-time constant, including something like this:

constexpr float MyMin(constexpr float a, constexpr float b) { return a<b?a:b; }

Because qualifying a function's return type only as constexpr does not limit its usage to compile-time, and must also be callable at runtime, I figured that this would be a way to ensure that MyMin can only ever be used with compile-time evaluated constants, and this would ensure that the compiler would never allow its execution at runtime, freeing me to write an alternative more runtime friendly version of MyMin, ideally with the same name that uses a _mm_min_ss intrinsic, ensuring that the compiler won't generate runtime branching code. Unfortunately, function parameters cannot be constexpr, so it would seem that this cannot be done, unless something like this were possible:

constexpr float MyMin(float a, float b)
{
#if __IS_COMPILE_TIME__
    return a<b?a:b;
#else
    return _mm_cvtss_f32(_mm_min_ss(_mm_set_ss(a),_mm_set_ss(b)));
#endif
}

I have serious doubts that MSVC++ has anything like this at all, but I was hoping maybe GCC or clang have at least something to accomplish it, however unelegant it may look like.

Granted, the example I presented was very simplistic, but if you can use your imagination, there are many cases where you could feel free to do something like make extensive use of branching statements within a function that you know can only execute at compile time, because if it executed at runtime, performance would be compromised.

It is possible to detect if a given function-call expression is a constant expression, and thereby select between two different implementations. Requires C++14 for the generic lambda used below.

(This answer grew out this answer from @Yakk to a question I asked last year).

I'm not sure how far I'm pushing the Standard. This is tested on clang 3.9, but causes g++ 6.2 to give an "internal compiler error". I'll send a bug report next week (if nobody else does it first!)

This first step is to move the constexpr implementation into a struct as a constexpr static method. More simply, you could leave the current constexpr as is and call it from a constexpr static method of a new struct .

struct StaticStruct {
    static constexpr float MyMin_constexpr (float a, float b) {
        return a<b?a:b;
    }
};

Also, define this (even though it looks useless!):

template<int>
using Void = void;

The basic idea is that Void<i> requires that i be a constant expression. More precisely, this following lambda will have suitable overloads only in certain circumstances:

auto l = [](auto ty)-> Void<(decltype(ty)::   MyMin_constexpr(1,3)   ,0)>{};
                                              \------------------/
                                               testing if this
                                               expression is a
                                               constant expression.

We can call l only if the argument ty is of type StaticStruct and if our expression of interest ( MyMin_constexpr(1,3) ) is a constant expression. If we replace 1 or 3 with non-constant arguments, then the generic lambda l will lose the method via SFINAE.

Therefore, the following two tests are equivalent:

  • Is StaticStruct::MyMin_constexpr(1,3) a constant expression ?
  • Can l be called via l(StaticStruct{}) ?

It's tempting to simply delete auto ty and decltype(ty) from the above lambda. But that will give a hard error (in the non-constant case) instead of a nice substitution failure. We therefore use auto ty to get substitution failure (which we can usefully detect) instead of error.

This next code is a straightforward thing to return std:true_type if and only if f (our generic lambda) can be called with a ( StaticStruct ):

template<typename F,typename A>
constexpr
auto
is_a_constant_expression(F&& f, A&& a)
    -> decltype( ( std::forward<F>(f)(std::forward<A>(a)) , std::true_type{} ) )
{ return {}; }
constexpr
std::false_type is_a_constant_expression(...)
{ return {}; }

Next, a demonstration of it's use:

int main() {
    {
        auto should_be_true = is_a_constant_expression(
            [](auto ty)-> Void<(decltype(ty)::   MyMin_constexpr(1,3)   ,0)>{}
            , StaticStruct{});
        static_assert( should_be_true ,"");
    }
    {   
        float f = 3; // non-constexpr
        auto should_be_false = is_a_constant_expression(
            [](auto ty)-> Void<(decltype(ty)::   MyMin_constexpr(1,f)   ,0)>{}
            , StaticStruct{});
        static_assert(!should_be_false ,"");
    }
}

To solve your original problem directly, we could first define a macro to save repetition:

(I haven't tested this macro, apologies for any typos.)

#define IS_A_CONSTANT_EXPRESSION( EXPR )                \
     is_a_constant_expression(                          \
         [](auto ty)-> Void<(decltype(ty)::             \
              EXPR                         ,0)>{}       \
         , StaticStruct{})

At this stage, perhaps you could simply do:

#define MY_MIN(...)                                            \
    IS_A_CONSTANT_EXPRESSION( MyMin_constexpr(__VA_ARGS__) ) ? \
        StaticStruct :: MyMin_constexpr( __VA_ARGS__ )     :   \
                        MyMin_runtime  ( __VA_ARGS__ )

or, if you don't trust your compiler to optimize std::true_type and std::false_type through ?: , then perhaps:

constexpr
float MyMin(std::true_type, float a, float b) { // called if it is a constant expression
    return StaticStruct:: MyMin_constexpr(a,b);
}
float MyMin(std::false_type, float , float ) { // called if NOT a constant expression
    return                MyMin_runtime(a,b);
}

with this macro instead:

#define MY_MIN(...)                                             \
  MyMin( IS_A_CONSTANT_EXPRESSION(MyMin_constexpr(__VA_ARGS__)) \
       , __VA_ARGS__)

I figured that this would be a way to ensure that MyMin can only ever be used with compile-time evaluated constants, and this would ensure that the compiler would never allow its execution at runtime

Yes; there is a way.

And works with C++11 too.

Google-ing I've found a strange poisoning way (by Scott Schurr): in short, the following

extern int no_symbol;

constexpr float MyMin (float a, float b)
 {
   return a != a ? throw (no_symbol)
                 : (a < b ? a : b) ;
 }

int main()
 {
   constexpr  float  m0 { MyMin(2.0f, 3.0f) }; // OK

   float  f1 { 2.0f };

   float  m1 { MyMin(f1, 3.0f) };  // linker error: undefined "no_symbol"
 }

If I understand well, the idea behind it is that if MyMin() is executed compile time, throw(no_symbol) is never used ( a != a is ever false) so there is no need to use no_symbol that is declared extern but is never defined (and throw() can't be used compile time).

If you use MyMin() run-time, throw(no_symbol) is compiled and no_symbol gives an error in linking phase.

More generally speaking, there is a proposal (ever from Scott Schurr) but I'm not aware of implementations.

--- EDIT ---

As pointed by TC (thanks!) this solution work (if works and when works) only because the compiler doesn't optimize at point to understand that a != a is ever false.

In particular, MyMin() works (without good optimizations) because, in the example, we're working with float numbers and a != a can be true if a is NaN, so it's more difficult for a compiler detect that the throw() part is unuseful. If MyMin() is a function for integers, the body can be written (with test float(a) != float(a) to try to obstruct the compliler optimizations) as

constexpr int MyMin (int a, int b)
 {
   return float(a) != float(a) ? throw (no_symbol)
                 : (a < b ? a : b) ;
 }

but isn't a real solution for a function where there ins't a "natural" throw-able error case.

When it's a natural error case that should give error (compiling or running), it's different: the compiler can't optimize and the trick work.

Example: if MyMin() return the minimun value between a and b but a and b are to be differents or MyMin() should give a compiler error (not a great example... I know), so

constexpr float MyMin (float a, float b)
 {
   return a != b ? throw (no_symbol)
                 : (a < b ? a : b) ;
 }

works because the compiler can't optimize a != b and must compile (giving linker error) the throw() part.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM