为什么我们需要将函数标记为constexpr?

时间:2013-01-23 04:15:55

标签: c++ c++11 constexpr

C ++ 11允许使用constexpr说明符声明的函数用于常量表达式,例如模板参数。关于允许的内容有constexpr的严格要求;本质上这样的函数只包含一个子表达式,而不包含任何其他子表达式。 (编辑:这在C ++ 14中是放松的,但问题在于。)

为什么要求关键字? 获得了什么?

它有助于揭示接口的意图,但它doesn't通过保证函数在常量表达式中可用来验证该意图。在编写constexpr函数之后,程序员仍然必须:

  1. 编写测试用例或以其他方式确保它实际用于常量表达式。
  2. 记录常量表达式上下文中哪些参数值有效。
  3. 与揭示意图相反,使用constexpr装饰函数可能会增加错误的安全感,因为在忽略中心语义约束的同时检查了切向句法约束。


    简而言之:如果函数声明中的constexpr仅仅是可选的,那么会对语言产生任何不良影响吗?或者对任何有效的程序都会有任何影响吗?

4 个答案:

答案 0 :(得分:38)

防止客户端代码期望超出您的承诺

假设我正在编写一个库并且在那里有一个当前返回常量的函数:

  

awesome_lib.h:

inline int f() { return 4; }

如果不需要constexpr,您 - 作为客户端代码的作者 - 可能会离开并执行以下操作:

  

client_app.cpp:

#include <awesome_lib.h>
int my_array[f()];

然后,我应该更改f()以表示从配置文件返回值,您的客户端代码会中断,但我不知道我冒着破坏您的代码的风险。实际上,可能只有当你遇到一些生产问题并重新编译时才发现这个额外的问题会让你的重建受挫。

只更改f()实现,我已经有效地改变了接口的用法。

相反,C ++ 11以后提供constexpr所以我可以表示客户端代码可以合理地期望函数保持constexpr,并使用它本身。 我知道并认可这种用法作为我的界面的一部分。就像在C ++ 03中一样,编译器继续保证客户端代码不会依赖于其他非{{1用于防止上述“不需要/未知的依赖”场景的函数;这不仅仅是文档 - 它是编译时的执行。

值得注意的是,这延续了C ++为预处理器宏的传统使用提供更好替代方案的趋势(考虑constexpr,以及客户端程序员如何知道lib程序员是否认为公平游戏改为{{1} }},用他们众所周知的“邪恶”,例如在语言的命名空间/类范围系统之外。

为什么没有对“明显”永不变量的函数进行诊断?

我认为这里的混淆是由于#define F 4没有主动确保有任何一组参数,其结果实际上是编译时const:相反,它需要程序员对此负责(否则§标准中的7.1.5 / 5认为程序格式错误,但不要求编译器发出诊断信息。是的,这很不幸,但它没有删除#define F config["f"]的上述效用。

所以,也许问题不应该是“constexpr”的重点,而是“为什么我可以编译一个永远不能实际返回const值的constexpr函数?”。答:因为需要进行详尽的分支分析,这可能涉及任意数量的组合。编译时和/或内存(甚至超出任何可以想象的硬件的能力)诊断成本可能过高。此外,即使在实际必须准确诊断此类情况时,编译器编写者(已经在C ++ 11实现中已经足够忙)也是一种全新的蠕虫病毒。对程序也有影响,例如在执行验证时需要在constexpr函数中调用的函数的定义(以及函数调用函数等)。

同时,constexpr缺少继续禁止用作const值:严格性在sans - constexpr方面。如上图所示,这很有用。

与非`constst成员函数

的比较
  • constexpr阻止constexpr,而constexpr无法阻止int x[f()] - 他们都 确保客户端代码不会对不需要的内容进行硬编码依赖

  • 在这两种情况下, 您不希望编译器自动确定const -

    • 你不希望客户端代码调用const X x; x.f();对象上的成员函数,当你已经预料到函数将演变为修改可观察值,打破客户端代码

      < / LI>
    • 如果您以后在运行时确定了它,则不希望将值用作模板参数或数组维度

  • 他们 不同 ,因为编译器强制const[expr]使用const成员函数中的其他成员,但不强制执行const的编译时常量结果(由于实际的编译器限制)

答案 1 :(得分:14)

I pressed Clang作者理查德史密斯解释:

  

constexpr关键字确实有效用。

     

它会影响函数模板特化实例化的时间(如果在未评估的上下文中调用constexpr函数模板特化,则可能需要实例化;对于非constexpr函数,情况也是如此;因为对一个函数的调用永远不能成为其中一部分一个常数表达式)。如果我们删除了关键字的含义,我们必须尽早实例化更多的特化,以防万一调用恰好是一个常量表达式。

     

通过限制在翻译期间尝试评估所需的实现的函数调用集,减少了编译时间。 (这对于需要实现以进行常量表达式评估的上下文很重要,但如果此类评估失败则不是错误 - 特别是静态存储持续时间对象的初始化器。)

这一切看起来并不令人信服,但如果你仔细研究细节,那么事情就会在没有constexpr的情况下解开。在使用ODR之前,不需要实例化函数,这实际上意味着在运行时使用。 constexpr函数的特殊之处在于它们可以违反此规则并且无论如何都需要实例化。

函数实例化是一个递归过程。实例化一个函数会导致它使用的函数和类的实例化,而不管任何特定调用的参数。

如果在实例化此依赖关系树时出现问题(可能花费很大),则很难吞下错误。此外,类模板实例化可能会产生运行时副作用。

给定函数签名中依赖于参数的编译时函数调用,重载解析可能会导致函数定义的实例化仅仅是辅助过载集中的函数定义,包括甚至不被调用的函数。此类实例化可能具有副作用,包括不良形式和运行时行为。

这是一个确定的角落案例,但如果您不要求人们选择加入constexpr函数,就会发生不好的事情。

答案 2 :(得分:4)

如果没有关键字,编译器就无法诊断错误。编译器无法告诉您该函数在语法上与constexpr无效。虽然你说这提供了“虚假的安全感”,但我认为最好尽早发现这些错误。

答案 3 :(得分:2)

我们可以在没有constexpr的情况下生活,但在某些情况下,它会使代码更容易和直观 例如,我们有一个类声明一个具有一些引用长度的数组:

template<typename T, size_t SIZE>
struct MyArray
{
  T a[SIZE];
};

传统上,您可以将MyArray声明为:

int a1[100];
MyArray<decltype(*a1), sizeof(a1)/sizeof(decltype(a1[0]))> obj;

现在看看它与constexpr

的关系
template<typename T, size_t SIZE>
constexpr
size_t getSize (const T (&a)[SIZE]) { return SIZE; }

int a1[100];
MyArray<decltype(*a1), getSize(a1)> obj;

简而言之,只有当编译器将其识别为getSize(a1)时,任何函数(例如constexpr)都可以用作模板参数。

constexpr也用于检查否定逻辑。它确保给定对象处于编译时。这是参考link,例如

int i = 5;
const int j = i; // ok, but `j` is not at compile time
constexprt int k = i; // error