【问题标题】:Is constexpr really needed?真的需要 constexpr 吗?
【发布时间】:2011-05-16 11:35:42
【问题描述】:

我一直在研究 C++ 的 constexpr 新特性,但我并不完全了解它的必要性。

例如下面的代码:

constexpr int MaxSize()
{
    ...

    return ...;
}

void foo()
{
    int vec[MaxSize()];
}

可以替换为:

int MaxSize()
{
    ...

    return ...;
}

static const int s_maxSize = MaxSize();

foo()
{
    int vec[s_maxSize];
}

更新

第二个示例实际上不是标准的 ISO C++(感谢一些用户指出这一点),但某些编译器(例如 gcc)支持它。因此,使程序有效的不是const,而是 gcc 支持这种非标准特性的事实。 (据我所知,这仅在数组被定义为函数或方法的本地时才有可能,因为在编译时仍必须知道全局数组的大小。)如果我在没有选项 -std=c++98 -pedantic-errors 的情况下进行编译,即使代码

int MaxSize()
{
    return 10;
}

void foo()
{
    int vec[MaxSize()];
}

将使用 gcc 编译。

因此,我将尝试重新表述我的问题,同时考虑到目前收到的反馈(以及我在此期间完成的一些进一步阅读)。

我大量使用const 关键字。使用const,我可以定义一个在其整个生命周期内具有特定值的常量。可以使用任何表达式初始化常量,该表达式只计算一次,即在创建常量时。对于这些情况,我认为constexpr 毫无用处:它会引入一个非常小的优化,因为定义常量值的表达式将在编译时而不是运行时计算。每次我需要一个带有复杂初始化的运行时常量时,我​​都会使用关键字const

所以constexpr 在我们需要在编译时初始化一个常量的情况下可能会派上用场。一个例子是向量定义:标准不支持在运行时定义大小。另一个例子是带有一个或多个非类型参数的模板。

在这种情况下,我通常使用宏:

#define MAX_SIZE (10)

void foo()
{
    int vec[MAX_SIZE];
}

但是,如果我理解正确的话,constexpr 函数比宏更强大,因为它们允许在其定义中递归调用 constexpr 函数。但是,我想不出任何实际应用中我曾经想使用如此复杂的计算来定义编译时常量。

因此,即使它可能是一个有趣的功能,我仍然想知道是否需要它(即它多久可以解决宏不够用的情况)。也许看看一些无法用宏解决的现实例子会帮助我改变这种看法。

【问题讨论】:

  • 实际上,我的示例不起作用。我必须在函数或对象中声明数组。我一直在用这个成语。
  • @Giorgio:嗯?这如何使其合法化?数组必须使用常量表达式的大小进行初始化。一些编译器(即 GCC)允许您使用动态大小的数组,这是 C99 的一项功能,不是 C++ 的一部分。您能否编辑您的帖子以阐明您将如何做到这一点?
  • 如果这里有一个您认为足够的答案,您最好接受它。 :-)
  • 我在实现 AES 算法时使用了 constexpr。您必须计算许多元素,它们始终是恒定的,但元素的数量是不同的。我选择计算 256 个数字。它花费了 150 万纳秒(1.5 毫秒),在使用 constexpr 后,它在 1 个处理器滴答声中执行(在我的情况下为 513 纳秒)。而且有时您必须进行更复杂的操作...
  • 我实现了一个 SIMD 置换库,其中用户根据位置换(即超立方体置换或位置换/补置换)指定置换,SIMD 代码通过模板扩展生成。需要计算逆排列。如果没有 constexpr,另一种方法是实现一个完整的模板元编程库,用于整数和序列处理。

标签: c++ c++11 constexpr


【解决方案1】:

int vec[s_maxSize]; 在第二个例子中实际上是非法的,所以这在 C++ 中是不可能的。但是你的第一个例子是完全合法的 C++0x。

这就是你的答案。你实际上不能按照你在 C++ 中提出的建议去做。它只能在 C++0x 中使用constexpr 完成。

我还想指出,这段代码也适用于 C++0x。在 C++ 中执行此操作需要一些非常精美的类模板。

constexpr unsigned int gcd(unsigned int const a, unsigned int const b)
{
   return (a < b) ? gcd(b, a) : ((a % b == 0) ? b : gcd(b, a % b));
}

char vec[gcd(30, 162)];

当然,在 C++0x 中,您仍然必须使用三元运算符而不是 if 语句。但是,它可以工作并且仍然比您在 C++ 中强制使用的模板版本更容易理解:

template <unsigned int a, unsigned int b>
class gcdT {
 public:
   static unsigned int const value = gcdT<b, a % b>::value;
};

template <unsigned int a>
class gcdT<a, 0> {
 public:
   static unsigned int const value = a;

};

char vec[gcdT<30, 162>::value];

然后,当然,在 C++ 中,如果您需要在运行时计算事物,您仍然必须编写 gcd 函数,因为模板不能与在运行时变化的参数一起使用。并且 C++0x 将有额外的优化提升,因为知道函数的结果完全由传入的参数决定,这是只能用 C++ 中的编译器扩展来表达的事实。

【讨论】:

  • 是的,我的例子是错误的,我通常在函数或类构造函数中使用这个成语和变量声明(例如 int vec[])。我用它所有的时间。以这种方式声明全局数组在 C++ 中是非法的。
  • 因此您需要 constexpr 来定义具有非类型参数的模板,这样您就不必计算常量的值,因为您可以让编译器计算它。是否有任何其他示例必须在编译时知道一个值并且需要确定一个非平凡的计算?
  • 也许 constexpr 的意思是一个纯函数:一个不依赖任何 I/O 并且不会产生任何副作用的函数?这意味着它只产生一个结果,并且结果只取决于输入参数的值。在这种情况下,可以在整个代码中执行各种编译时优化。但我猜编译器变得非常复杂。在第一次通过时,它必须编译程序中的所有函数。然后它必须遍历所有调用 constexpr 函数的地方,评估它们,并用结果替换调用。
  • @Giorgio - 好吧,我认为另一个规则是,为了被视为constexpr 函数,编译器在调用时必须可以使用完整的定义。编译器已经使用内联函数做到了这一点。只是constexpr 函数提供了另一个级别的提示,允许编译器更加激进。例如,constexpr 函数可以参与 CSE,编译器甚至无需查看它们的定义。
  • @Giorgio:作为一个实际的例子,我最近编写了一组用于计算整数对数和指数的模板,用于计算出在一个小范围内有多少随机数可以有效地从随机数中提取出来。具有一定位数的数字。假设您想要一个 1-6 的数字,并且您有一个 32 位随机数,您可以将其视为以 6 为底的 12 位数字,并从中获得 12 个随机数字。使用 constexpr 会容易得多。
【解决方案2】:

你可以用 constexpr 做一些你不能用宏或模板做的事情是在编译时解析/处理字符串:Compile time string processing (changing case, sorting etc.) with constexpr。作为前面链接的一小段摘录,constexpr 允许编写如下代码:

#include "my_constexpr_string.h"
int main()
{
   using namespace hel;
   #define SDUMP(...) static_assert(__VA_ARGS__, "")

   SDUMP(tail("abc") == "bc");
   SDUMP( append("abc", "efgh") == "abcefgh" );
   SDUMP( prepend("abc", "efgh") == "efghabc" );
   SDUMP( extract<1,3>("help") == "el" );
   SDUMP( insert<1>("jim", "abc") == "jabcim" );
   SDUMP( remove("zabzbcdzaz", 'z') == "abbcdazzzz" );
   SDUMP( erase("z12z34z5z", 'z') == "12345"  );
   SDUMP( map("abc", ToUpper()) == "ABC" );
   SDUMP( find("0123456777a", '7') == 7 );
   SDUMP( isort("03217645") == "01234567");  
}

作为什么时候有用的一个例子,它可以促进某些解析器和正则表达式有限状态机的编译时计算/构造,这些解析器是用文字字符串指定的。而且,您可以在编译时进行的处理越多,您在运行时执行的处理就越少。

【讨论】:

  • 不错。这是另一种方法:stackoverflow.com/questions/4583022/…。更整洁的版本:ideone.com/DWCRB
  • 使用示例:命名参数,ideone.com/tgapx。可悲的是,GCC4.6 仍然大喊“对不起,未实现:在模板中使用 'type_pack_expansion'”。所以我无法测试它,但我认为它应该可以工作。等待 clang 实现这个...
  • @Faisal Vali 你的意思是你的词法分析器规范中有字符串文字,并且你想在编译时使用 const_expr 函数来减少这些文字?
  • 这对于生成的二进制文件中的字符串混淆也非常有用。
【解决方案3】:
int MaxSize() {
    ...

    return ...; }

static const int s_maxSize = MaxSize();

int vec[s_maxSize];

不,它不能。这不是合法的 C++03。您有一个可以分配可变长度数组的编译器扩展。

【讨论】:

    【解决方案4】:

    constexpr 允许 if 在编译时检测 undefined behavior 的另一个巧妙技巧,这看起来是一个非常有用的工具。以下示例取自我链接的问题,使用 SFINAE 来检测添加是否会导致溢出:

    #include <iostream>
    #include <limits>
    
    template <typename T1, typename T2>
    struct addIsDefined
    {
         template <T1 t1, T2 t2>
         static constexpr bool isDefined()
         {
             return isDefinedHelper<t1,t2>(0) ;
         }
    
         template <T1 t1, T2 t2, decltype( t1 + t2 ) result = t1+t2>
         static constexpr bool isDefinedHelper(int)
         {
             return true ;
         }
    
         template <T1 t1, T2 t2>
         static constexpr bool isDefinedHelper(...)
         {
             return false ;
         }
    };
    
    
    int main()
    {    
        std::cout << std::boolalpha <<
          addIsDefined<int,int>::isDefined<10,10>() << std::endl ;
        std::cout << std::boolalpha <<
         addIsDefined<int,int>::isDefined<std::numeric_limits<int>::max(),1>() << std::endl ;
        std::cout << std::boolalpha <<
          addIsDefined<unsigned int,unsigned int>::isDefined<std::numeric_limits<unsigned int>::max(),std::numeric_limits<unsigned int>::max()>() << std::endl ;
    }
    

    导致 (see it live):

    true
    false
    true
    

    【讨论】:

    • 给定extern int foo[];,如果foo 的元素少于三个,则表达式foo+3-foo 会调用UB,但我想不出任何方式可以期望编译器拒绝任何会尝试的模板扩展计算。
    【解决方案5】:

    constexpr 允许以下操作:

    #include<iostream>
    using namespace std;
    
    constexpr int n_constexpr() { return 3; }
    int n_NOTconstexpr() { return 3; }
    
    
    template<size_t n>
    struct Array { typedef int type[n]; };
    
    typedef Array<n_constexpr()>::type vec_t1;
    typedef Array<n_NOTconstexpr()>::type vec_t2; // fails because it's not a constant-expression
    
    static const int s_maxSize = n_NOTconstexpr();
    typedef Array<s_maxSize>::type vec_t3; // fails because it's not a constant-expression
    

    template 参数确实需要是常量表达式。您的示例有效的唯一原因是可变长度数组 (VLA) - 标准 C++ 中没有的功能,但可能在许多编译器中作为扩展。

    一个更有趣的问题可能是:为什么不将constexpr 放在每个(const)函数上?有没有害处!?

    【讨论】:

      【解决方案6】:

      根据这种推理,您通常不需要常量,甚至不需要#define。没有内联函数或任何东西。

      constexpr 的意义与许多关键字一样,是为了让您表达您的意图更好,以便编译器准确地理解您想要的,而不仅仅是您想要的'正在告诉它,所以它可以在幕后为你做更好的优化。

      在此示例中,它允许您编写可维护的函数来计算矢量大小,而不仅仅是您一遍又一遍地复制和粘贴的纯文本。

      【讨论】:

      • -1 - 这根本不能真正回答 OP 的问题,而且是不必要的尖酸刻薄。
      • 我不反对常量:我一直都在使用它们。我需要知道一个名字有一个特定的值,并且这个值不会改变。如果我理解正确, constexpr 的意思是:亲爱的编译器,这看起来像是一个需要在运行时评估的表达式(因为它可能需要用户的输入),但它实际上不是并且可以在编译时评估。因此,这将在程序启动时节省几毫秒的时间,因为相同的表达式可以在程序启动时进行评估并分配给 const 变量。
      • @Giorgio,基本上是的,但它还允许您将函数值常量分配给数组大小,例如,因为您无法在 C++ 中创建可变大小的数组。然后是整个维护方面。
      • @Bindy:经常需要这个吗?也许大多数时候宏就足够了?我一直为此使用宏,并且从未觉得需要更强大的东西。宏可以相互调用,但据我所知,不能递归使用。这个限制是不是太强了?
      • 不知道为什么到目前为止这被否决了,但是虽然我同意陈述意图和允许可选优化是constexpr 的基本原理中的关键要素 - 它们显然不是“重点”,如果我们只得选一个。 (正如其他答案中已经强调的那样,“最好的理由”肯定是启用诸如数组维度之类的编译时功能。)
      猜你喜欢
      • 2012-03-13
      • 2020-01-29
      • 2014-02-28
      • 2011-01-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-19
      • 2011-04-21
      相关资源
      最近更新 更多