为什么我们需要将函数标记为 constexpr
Why do we need to mark functions as constexpr?
C++11 允许在常量表达式(如模板参数)中使用用constexpr
说明符声明的函数。对允许constexpr
的内容有严格的要求;本质上,这样的函数只封装了一个子表达式,没有其他任何东西。(编辑:这在C++14中有所放松,但问题仍然存在。
为什么需要关键字?收获了什么?
它确实有助于揭示接口的意图,但它不会通过保证函数在常量表达式中可用来验证该意图。编写constexpr
函数后,程序员仍必须:
- 编写测试用例或以其他方式确保它实际用于常量表达式中。
- 记录哪些参数值在常量表达式上下文中有效。
与揭示意图相反,用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
- lambda参数转换为constexpr技巧,然后获取带链接的数组
- 是否可以将带有字符串化运算符的宏转换为 constexpr?
- 将INT32BE宏转换为 constexpr 是否正确?
- 为什么 std::array 的运算符 ==() 没有标记为 constexpr?
- 将 'hana::string' 转换为 'constexpr const char (&)[]'
- 如果我的班级是字面的班级,那么将我的类的对象声明为constexpr是多余的
- 将结构转换为 constexpr 数组uint8_t
- 将默认赋值运算符声明为 constexpr:哪个编译器是正确的?
- 将variadic模板转换为constexpr
- 将set函数(setter)标记为constexpr的目的是什么
- 是否可以在 constexpr 函数中遍历枚举成员,因此值为 constexpr
- 显式默认函数不能声明为 constexpr,因为隐式声明不是 constexpr
- 将静态 const 成员重新声明为 constexpr 是否会自动使其符合内联条件
- 是否可以将类型别名定义为constexpr函数
- 如何将模板大小的数组初始化转换为 constexpr 初始化
- 我应该将编译器生成的构造函数标记为 constexpr 吗?
- 只要调用的函数是用constexpr指定的,就将委托方法声明为constexpr
- static_assert 无法将 const char* 模板参数识别为 constexpr:g++ 错误?
- 将代码转换为 constexpr
- 什么时候应该将构造函数设置为 constexpr