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

Why do we need to mark functions as constexpr?

本文关键字:记为 constexpr 函数 我们 为什么      更新时间:2023-10-16

C++11 允许在常量表达式(如模板参数)中使用用constexpr说明符声明的函数。对允许constexpr的内容有严格的要求;本质上,这样的函数只封装了一个子表达式,没有其他任何东西。(编辑:这在C++14中有所放松,但问题仍然存在。

为什么需要关键字?收获了什么?

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

  1. 编写测试用例或以其他方式确保它实际用于常量表达式中。
  2. 记录哪些参数值在常量表达式上下文中有效。

与揭示意图相反,用constexpr装饰函数可能会增加一种虚假的安全感,因为检查切线句法约束而忽略中心语义约束。


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

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

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

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有合理的期望,并以此使用它。 我知道并认可这种用法作为我界面的一部分。 就像在 C++03 中一样,编译器继续保证客户端代码不会依赖于其他非constexpr函数来防止上述"不需要/未知的依赖项"情况;这不仅仅是文档 - 它是编译时强制实施。

值得注意的是,这延续了为预处理器宏的传统用途提供更好的替代方案的C++趋势(考虑#define F 4,以及客户端程序员如何知道lib程序员是否认为改变说#define F config["f"]是公平的游戏),以及它们众所周知的"邪恶",例如在语言的命名空间/类范围系统之外。

为什么没有针对"明显"永不常量的功能的诊断?

我认为这里的混淆是由于constexpr没有主动确保有任何一组参数的结果实际上是编译时常量:相反,它要求程序员对此负责(否则标准中的 §7.1.5/5 认为程序格式不正确,但不要求编译器发出诊断)。 是的,这很不幸,但它并没有消除上述constexpr实用程序。

因此,也许从问题"constexpr的意义何在"切换到考虑"为什么我可以编译一个永远不会实际返回编译时值的constexpr函数?"是有帮助的。

答:因为需要详尽的分支分析,这可能涉及任意数量的组合。 在编译时间和/或内存方面,诊断成本可能过高 - 甚至超出了任何可以想象的硬件的能力。 此外,即使实际上必须准确诊断这种情况对于编译器编写者(他们有更好的时间利用)来说是一个全新的蠕虫罐头。 对程序也有影响,例如从constexpr函数中调用的函数的定义需要在执行验证时可见(以及函数调用的函数等)。

与此同时,缺乏constexpr继续禁止用作编译时值:严格性是无constexpr的。 如上图所示,这很有用。

与非"常量"成员函数的比较

  • constexpr可以防止int x[f()],而缺乏const可以防止const X x; x.f(); - 它们都确保客户端代码不会硬编码不需要的依赖项/用法

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

    • 您不希望客户端代码调用 const 对象上的成员函数,因为您已经可以预期该函数将演变为修改可观察值,从而破坏客户端代码

    • 如果您已经预料到稍后会在运行时确定值,则不希望将值用作模板参数或数组维度

  • 它们的不同之处在于,编译器强制const const成员函数中使用其他成员,但不强制使用constexpr编译时常量结果(由于实际编译器的限制,也许一个函数定义可以愉快地为运行时已知的参数/值提供运行时结果,但在可能和客户端使用需要时返回编译时结果)。

当我追问Clang的作者理查德·史密斯(Richard Smith)时,他解释说:

constexpr关键字确实有用。

它会影响函数模板专用化

的时间(如果在未计算的上下文中调用 constexpr 函数模板专用化,则可能需要实例化它们;对于非 constexpr 函数来说也是如此,因为对 1 的调用永远不能成为常量表达式的一部分)。如果我们删除了关键字的含义,我们将不得不尽早实例化更多的专业化,以防调用恰好是一个常量表达式。

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

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

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

如果在实例化此依赖树时出现问题(可能会付出巨大的代价),则很难接受错误。此外,类模板实例化可能会产生运行时副作用。

给定函数签名中依赖于参数的编译时函数调用,重载解析可能会导致函数定义的实例化,这些函数定义仅辅助于重载集中的函数定义,包括甚至未被调用的函数。此类实例化可能会产生副作用,包括格式错误和运行时行为。

可以肯定的是,这是一个极端情况,但如果您不要求人们选择加入constexpr功能,则可能会发生不好的事情。

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

我们可以没有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