constexpr函数中的编译时或运行时检测

Compile-time or runtime detection within a constexpr function

本文关键字:运行时 检测 编译 函数 constexpr      更新时间:2023-10-16

当constexpr在c++ 11中引入时,我很兴奋,但不幸的是,我对它的有用性做出了乐观的假设。我假设我们可以在任何地方使用constexpr来捕获文字编译时常量或文字编译时常量的任何constexpr结果,包括如下内容:

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

因为将函数的返回类型限定为constexpr并不限制其在编译时的使用,并且还必须在运行时可调用,我认为这将是一种确保MyMin只能与编译时求值的常量一起使用的方法,这将确保编译器永远不会允许它在运行时执行,从而使我可以编写另一个更适合运行时的MyMin版本,理想情况下使用与_mm_min_ss相同的名称。确保编译器不会生成运行时分支代码。不幸的是,函数参数不能被constexpr,所以这似乎是不可能的,除非像这样:

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
}

我非常怀疑msvc++是否有这样的功能,但我希望GCC或clang至少有一些功能来完成它,无论它看起来多么不优雅。

当然,我给出的例子非常简单,但是如果您可以发挥您的想象力,在许多情况下,您可以自由地做一些事情,例如在您知道只能在编译时执行的函数中广泛使用分支语句,因为如果它在运行时执行,性能将受到损害。

可以检测给定的函数调用表达式是否为常量表达式,从而在两种不同的实现之间进行选择。下面使用的泛型lambda需要c++ 14。

(这个答案来自@Yakk对我去年问过的一个问题的回答)。

我不确定我在多大程度上推动了标准。这在clang 3.9上进行了测试,但会导致g++ 6.2给出"内部编译器错误"。我将在下周发送一个bug报告(如果没有其他人先做的话!)

第一步是将constexpr实现作为constexpr static方法移动到struct中。更简单地说,您可以让当前的constexpr保持原样,并从新的structconstexpr static方法调用它。

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

同样,定义这个(即使它看起来没用!):

template<int>
using Void = void;

基本思想是Void<i>要求i是一个常量表达式。更准确地说,以下lambda仅在某些情况下才具有合适的重载:

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

只有当实参ty是类型为StaticStruct 且感兴趣的表达式(MyMin_constexpr(1,3))是常量表达式时,才能调用l。如果我们用非常量参数替换13,那么泛型lambda l将通过SFINAE失去该方法。

因此,以下两个测试是等价的:

  • StaticStruct::MyMin_constexpr(1,3)常量表达式吗?
  • l可以通过l(StaticStruct{})调用吗?

很容易从上面的lambda中删除auto tydecltype(ty)。但这将导致一个严重的错误(在非常数情况下),而不是一个漂亮的替换失败。因此,我们使用auto ty来获得替换失败(我们可以有效地检测)而不是错误。

下一段代码返回std:true_type很简单当且仅当f(我们的通用lambda)可以被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 {}; }

接下来,演示它的用法:

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 ,"");
    }
}

为了直接解决你最初的问题,我们可以首先定义一个宏来避免重复:

(我还没有测试这个宏,为任何拼写错误道歉。)

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

在这个阶段,也许您可以简单地执行:

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

或者,如果您不相信您的编译器可以通过?:来优化std::true_typestd::false_type,那么可能:

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);
}

用宏代替:

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

我认为这将是一种确保MyMin只能用于编译时求值常量的方法,这将确保编译器永远不会允许它在运行时执行

是的;有办法的。

也可以在c++ 11中使用。

谷歌我发现了一种奇怪的中毒方式(作者:Scott Schurr):简而言之,如下

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"
 }

如果我理解得好,它背后的想法是,如果MyMin()在编译时执行,throw(no_symbol)永远不会被使用(a != a永远是假的),所以没有必要使用被声明为extern但从未定义的no_symbol (throw()不能在编译时使用)。

如果您使用MyMin()运行时,throw(no_symbol)被编译,no_symbol在链接阶段给出错误。

更一般地说,有一个建议(来自Scott Schurr),但我不知道实现。

—EDIT—

正如T.C.(谢谢!)所指出的那样,这个解决方案工作(如果工作和何时工作)只是因为编译器在这一点上没有优化以理解a != a是假的。

特别地,MyMin()工作(没有良好的优化),因为在这个例子中,我们使用浮点数,如果a是NaN, a != a可以为真,所以编译器更难检测到throw()部分无用。如果MyMin()是一个整数函数,则可以将函数体写成

(通过测试float(a) != float(a)来阻止编译器的优化)
constexpr int MyMin (int a, int b)
 {
   return float(a) != float(a) ? throw (no_symbol)
                 : (a < b ? a : b) ;
 }

但是对于没有"自然的"可抛出错误情况的函数来说并不是一个真正的解决方案。

当它是一个自然错误的情况下,应该给出错误(编译或运行),这是不同的:编译器不能优化和技巧工作。

示例:如果MyMin()返回ab之间的最小值,但ab是不同的,或者MyMin()应该给出编译错误(不是一个很好的例子…我知道),所以

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

工作,因为编译器不能优化a != b,必须编译(给出链接器错误)throw()部分。