54

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

为什么需要关键字?得到了什么?

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

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

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


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

4

4 回答 4

44

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

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

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,因此我可以表示客户端代码可以对保留的函数有合理的期望 a constexpr,并按原样使用它。 我知道并赞同将这种用法作为我界面的一部分。 就像在 C++03 中一样,编译器继续保证客户端代码不会构建为依赖于其他非constexpr函数,以防止上述“不需要/未知的依赖”情况;这不仅仅是文档 - 它是编译时执行。

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

为什么没有“明显”从不 const 函数的诊断?

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

所以,也许从“有什么意义”constexpr这个问题切换到考虑“为什么我可以编译一个constexpr永远不会真正返回 const 值的函数?”会有所帮助。.

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

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

与非`const`成员函数的比较

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

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

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

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

  • 它们的不同之处在于编译器强制const在成员函数中使用其他成员const,但不强制使用编译时常量结果constexpr(由于实际的编译器限制)

于 2013-01-23T04:39:26.953 回答
19

我向Clang 的作者 Richard Smith 提问时,他解释说:

constexpr 关键字确实具有实用性。

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

它通过限制实现在翻译期间尝试评估所需的函数调用集来减少编译时间。(这对于需要实现来尝试常量表达式求值的上下文很重要,但如果这种求值失败也不是错误——尤其是静态存储持续时间的对象的初始化程序。)

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

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

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

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

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

于 2013-11-07T08:02:05.887 回答
3

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

于 2013-01-23T04:29:57.130 回答
3

我们可以没有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
constexprt int k = i; // error
于 2013-01-23T04:36:11.900 回答