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

j2datikz  于 2023-05-24  发布在  其他
关注(0)|答案(4)|浏览(184)

C11允许用constexpr说明符声明的函数用于常量表达式,如模板参数。对什么是constexpr有严格的要求;本质上,这样的函数仅封装一个子表达式,而不封装其它任何子表达式。(编辑:这在C14中是宽松的,但问题仍然存在。
为什么需要关键字?获得了什么?
它确实有助于揭示接口的意图,但它doesn't通过保证函数在常量表达式中可用来验证该意图。在编写constexpr函数后,程序员仍然必须:
1.编写一个测试用例,或者确保它实际上在常量表达式中使用。
1.记录在常量表达式上下文中哪些参数值有效。
与揭示意图相反,用constexpr修饰函数可能会增加一种错误的安全感,因为在忽略中心语义约束的同时检查了切线语法约束。

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

qco9c6ql

qco9c6ql1#

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

假设我正在编写一个库,其中有一个函数当前返回一个常量:

awesome_lib.hpp

inline int f() { return 4; }

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

client_app.cpp

#include <awesome_lib.hpp>
#include <array>

std::array<int, f()> my_array;   // needs CT template arg
int my_c_array[f()];             // needs CT array dimension

然后,如果我将f()更改为从配置文件返回值,您的客户端代码将中断,但我不知道我冒了中断代码的风险。实际上,只有当您遇到一些生产问题并进行重新编译时,您才会发现这个额外的问题阻碍了您的重建。
通过只更改f()的 * 实现 ,我将有效地更改 * 接口 * 的用法。
相反,C++11以后提供了constexpr,所以我可以表示客户端代码可以合理地期望函数保持为constexpr,并这样使用它。
就像在C03中一样,编译器继续保证客户端代码不会依赖于其他非constexpr函数,以防止上面的“不需要的/未知的依赖”场景;这不仅仅是文档-它是编译时强制。
值得注意的是,这延续了C
的趋势,为预处理器宏的传统使用提供了更好的替代方案(考虑#define F 4,以及客户端程序员如何知道lib程序员是否认为更改为#define F config["f"]是公平的),其众所周知的“缺点”,例如在语言的命名空间/类范围系统之外。

为什么没有针对“明显”never-const函数的诊断?

我认为这里的混乱是由于constexpr没有主动确保有任何参数集的结果实际上是编译时常量:相反,它要求程序员对此负责(否则标准中的§7.1.5/5认为程序是病态的,但不要求编译器发出诊断)。是的,这是不幸的,但它并没有删除constexpr的上述实用程序。
所以,也许从“constexpr的意义是什么”这个问题转换到“为什么我可以编译一个永远不能返回常量值的constexpr函数”这个问题会有所帮助。“*.
回答:因为需要进行详尽的分支分析,可能涉及任何数量的组合。它可能在编译时间和/或内存上花费过多-甚至超出任何可以想象的硬件的能力-以进行诊断。此外,即使实际上必须准确地诊断这种情况,对于编译器编写人员(他们有更好的时间利用)来说也是一个全新的蠕虫。这也会对程序产生影响,例如从constexpr函数中调用的函数的定义需要在执行验证时可见(以及函数调用的函数等)。
同时,constexprlack 继续禁止作为const值使用:严格性在SANS-X1 M16 N1 X侧。如上所述,这很有用。

与非const成员函数的比较

  • constexpr防止int x[f()],而缺少const防止const X x; x.f();-它们都***确保客户端代码不会硬编码不需要的依赖项***
  • 在这两种情况下,你不希望编译器自动确定const[expr]-ness
  • 当您已经可以预期函数将演变为修改可观察值,从而破坏客户端代码时,您不希望客户端代码调用const对象上的成员函数
  • 如果您已经预期稍后在运行时确定某个值,则不希望将该值用作模板参数或数组维度
  • 它们***不同之处***在于编译器强制constconst成员函数中使用其他成员,但不强制constexpr使用编译时常量结果(由于实际编译器限制)
u7up0aaq

u7up0aaq2#

当理查德·史密斯,一个Clang的作者,他解释说:
constexpr关键字确实具有实用性。
它会影响函数模板专门化的示例化时间(如果在未求值的上下文中调用constexpr函数模板专门化,则可能需要示例化它们;这对于非constexpr函数不是真的,因为对一个函数的调用永远不能是常量表达式的一部分)。如果我们去掉了关键字的含义,我们就必须尽早示例化更多的专门化,以防调用恰好是一个常量表达式。
它通过限制在翻译期间实现需要尝试计算的函数调用的集合来减少编译时间。(这对于需要实现尝试常量表达式求值的上下文很重要,但是如果这样的求值失败了也不是错误--特别是静态存储持续时间的对象的初始化器。
起初,这一切似乎并不令人信服,但如果您仔细研究细节,没有constexpr,事情确实会变得明朗起来。一个函数在被ODR使用之前不需要被示例化,这本质上意味着在运行时使用。constexpr函数的特殊之处在于,它们可能违反这一规则,无论如何都需要示例化。
函数示例化是一个递归过程。示例化一个函数会导致它使用的函数和类的示例化,而不管任何特定调用的参数是什么。
如果在示例化这个依赖关系树时出现了错误(可能会造成很大的损失),那么就很难接受这个错误。此外,类模板示例化可能具有运行时副作用。
给定函数签名中的参数依赖的编译时函数调用,重载解析可能会导致函数定义的示例化,这些函数定义只是重载集中的函数定义的辅助,包括甚至没有被调用的函数。这样的示例化可能具有副作用,包括格式错误和运行时行为。
这当然是一种极端的情况,但是如果您不要求人们选择加入constexpr函数,就会发生不好的事情。

vpfxa7rd

vpfxa7rd3#

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

xbp102n0

xbp102n04#

没有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也用于检查负逻辑。它确保给定对象在编译时。这里是参考链接,例如:

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

相关问题