为什么不将函数声明为“constexpr”?

65

如果一个函数只由return语句组成,那么它可以被声明为constexpr,因此,如果所有参数都是constexpr,并且其主体中仅调用constexpr函数,则允许在编译时进行评估。 有什么理由不将任何这样的函数声明为constexpr吗?

例如:

  constexpr int sum(int x, int y) { return x + y; }
  constexpr i = 10;
  static_assert(sum(i, 13) == 23, "sum correct");

能否提供一个例子,说明声明一个函数为constexpr会有什么坏处?


一些初始想法:

即使永远没有理由声明一个函数不是constexpr,我可以想象constexpr关键字具有过渡角色:在不需要编译时执行的代码中缺少它将允许不实现编译时执行的编译器仍然编译该代码(但在需要使用constexpr显式指定的需要的代码上可靠地失败)。

但我不理解的是:如果永远没有理由去声明一个函数不是constexpr,那么为什么标准库中不是每个函数都声明为constexpr呢?(你不能认为还没有足够的时间来做到这一点,因为对于所有函数来说这是易如反掌的——与为每个单独的函数决定是否使其成为constexpr相反。) --- 我知道N2976故意不要求许多标准库类型(例如容器)具有cstrs,因为这对可能的实现来说太限制了。让我们将它们从参数中排除,只是想知道:一旦标准库中的类型实际上具有constexpr的cstr,为什么不将作用于它的每个函数声明为constexpr呢?

在大多数情况下,您也不能认为您可能不希望声明一个函数为constexpr,只是因为您不预计任何编译时使用:因为如果其他人可能会使用您的代码,则他们可能会看到您没有看到的这样的用途。(但当然适用于类型特征类型和类似的东西。)

所以我想肯定有一个好的理由和一个好的例子来故意不声明一个函数为constexpr吗?

(对于“每个函数”,我总是指:符合成为constexpr的要求的每个函数,即定义为单个返回语句,仅使用具有constexpr cstrs类型的参数并调用仅使用constexpr函数的函数。自C++14以来,在这样的函数体中允许更多内容:例如,C++14 constexpr函数可以使用本地变量和循环,因此可以声明一个更广泛的函数类为constexpr。)

问题为什么std::forward会丢弃constexpr是这个问题的一个特例。


我的问题是:如果声明为constexpr的函数在调用时并没有产生常量表达式,那么会发生什么?这不应该是编译错误吗?因此,对于那些不特别旨在在编译时进行评估的函数,它们不应该被声明为constexpr吗? - user534498
我查阅了标准,但没有找到提示说明如果非const表达式参数调用constexpr函数会发生什么。无论如何,如果这是错误的话,那么std::forward的情况也很清楚,如果您定义了std::forward constexpr,则必须将其用作constexpr,并且不能转发普通变量。 - user534498
1
@user534498 我也找不到标准中的相关内容。但唯一有意义的事情(也是g++实际执行的)是在使用非constexpr参数调用constexpr函数时,静默地忽略constexpr。否则像std::bitset中的size函数显然就没有必要成为constexpr了。 - Lars
@Lars:不仅如此,我还注意到如果输出没有明确指定为constexpr,则g++会忽略constexpr,而不管输入是否为constexpr。虽然我不确定这是否符合标准的意图,但对我来说这毫无意义。例如,将constexpr函数的返回值分配给const int将导致该函数出现在二进制文件中并被执行,而将其分配给一个枚举(具有相同的输入!)只是定义了一个枚举值,并且根本不生成代码。 - Damon
3个回答

38

如果一个函数符合constexpr的规则,那么才能声明它为constexpr --- 不能使用动态转换,不能进行内存分配,不能调用非constexpr函数等等。

在标准库中声明一个函数为constexpr需要保证所有实现都遵循这些规则。

首先,这要求对每个函数进行检查,以确定它是否可以作为constexpr实现,这是一项繁琐的工作。

其次,这对实现方法有很大的限制,并将禁止许多调试实现。因此,只有在好处大于成本或者要求足够严格,以至于实现基本上必须遵守constexpr规则时,才值得这样做。对于每个函数进行这种评估还是一项繁琐的工作。


1
我的意图并不是批评库工作组的工作,如果我的问题听起来像这样,我很抱歉。你所说的与接口有关,你是对的,标准库用许多实现描述了这样的接口。因此,可能查看STL以获取constexpr使用指南并不明智。我的问题是关于实现的。您是否同意实现应尽可能为constexpr?或者实现不是constexpr的例子吗?(抱歉,可能我没有理解您的观点...) - Lars
2
将某些内容设置为“constexpr”是相当具有限制性的。这不是我轻易会做的事情,因为一旦它成为您接口的一部分,由于向后兼容性的考虑,更改它将变得困难。即使对于标准库的特定实现,这也适用 - 很多人会无意中依赖于他们使用的库的实现属性,因此我不希望实现除了那些标准要求的函数外,使任何其他函数成为“constexpr”,或者不存在违反约束条件的合理实现。 - Anthony Williams
1
你好!抱歉,我还是不太明白,请问你能给一个代码示例吗?如果我们在一个函数中声明了constexpr,而这个函数可能会在编译器评估后成为constexpr或非constexpr,那么为什么不在每个可能以这种方式编译的函数中声明它(如果我们想要在编译时运行更多而不是运行时)? - Pedro Reis
2
首先,constexpr 函数隐式地是 inline 的,因此如果您不想将函数定义放在头文件中,则不能将其设置为 constexpr。其次,声明一个函数为 constexpr 意味着您不能使用 try 块、static 变量、thread_local 变量或非字面类型的变量,因此不能使用 std::stringstd::vector 等。 - Anthony Williams
2
如果您有一个inline函数可以是constexpr,那么将其声明为constexpr不会有任何副作用,除非它可能会对未来造成限制 --- 更改实现使其不兼容constexpr将导致ABI中断,因为在代码的某个地方它可能会用于常量表达式。 - Anthony Williams
显示剩余2条评论

15

我认为你所提到的是“部分求值”。你所说的是有些程序可以分成两部分 - 一部分需要运行时信息,而另一部分不需要任何运行时信息,理论上你可以在运行程序之前完全求值不需要任何运行时信息的部分。有一些编程语言可以做到这一点。例如,D编程语言在编译器中内置了解释器,可以让你在编译时执行代码,只要满足某些限制条件。

实现部分求值存在一些主要挑战。首先,它会极大地复杂化编译器的逻辑,因为编译器需要能够模拟所有你可以放入可执行程序中的操作并在编译时完成。在最坏的情况下,这要求你在编译器内部具有完整的解释器,使得一个困难的问题(编写一个好的C++编译器)变得难以解决。

我认为当前关于constexpr的规范之所以如此,仅仅是为了限制编译器的复杂性。它所限定的情况相当简单易懂。没有必要在编译器中实现循环(这可能会引起一系列其他问题,例如如果在编译器内部遇到无限循环会发生什么)。它还避免了编译器可能不得不评估可能在运行时导致段错误的语句,例如跟随一个错误的指针。

还有一件需要记住的事情是,一些函数具有副作用,比如从cin读取或打开网络连接。像这样的函数从根本上讲不能在编译时进行优化,因为这样做需要运行时才能获得的知识。

总之,理论上不存在阻止你在编译时部分地评估C++程序的原因。实际上,人们经常这样做。例如,优化编译器本质上就是尽可能多地尝试执行此操作的程序。模板元编程是C++程序员试图在编译器内部执行代码的一个例子,而使用模板可以做一些很棒的事情,部分原因是模板规则构成了一种函数式语言,编译器更容易实现。此外,如果你考虑编译器作者时间和编程人员时间之间的权衡,模板元编程表明,如果你愿意让程序员在得到他们想要的东西时受苦,那么可以构建一个相当简单的语言(模板系统),并保持语言的复杂性简单。(我说“弱”是指“不特别具有表达力”,而不是计算理论意义上的“弱”。)
希望这有所帮助!

规则似乎没有禁止副作用。 - Ben Voigt
1
虽然这是一个非常有趣的答案(部分求值是一个大主题,每个编译器编写者都尝试过一点),但它似乎有些离题,因为问题更多地涉及到constexpr的使用(对于程序员)而不是它的影响。 - Matthieu M.
@Matthieu M.- 我可能误读了问题。我理解的问题是“为什么不设计constexpr,使得任何函数都可以是constexpr,如果编译器无法在编译时完成,则会报错?”从这个意义上讲,我认为这个答案是合适的。如果我离原始问题太远,我可以删除这篇文章。 - templatetypedef
1
我宁愿你离开这篇文章,它很有趣。例如,我不知道D语言嵌入了解释器 :) 我必须承认,我对于constexpr的要求有点模糊,我需要努力学习。 - Matthieu M.
1
@Matthieu M. 感谢您提供的信息。对我来说,关于 D 语言的事情也是新奇有趣的。但是 Matthieu 是正确的,您回答了这个问题:为什么 constexpr 仅限于由单个返回语句组成的函数?而我的问题是:我们现在是否必须将每个函数声明为 constexpr,或者是否存在任何会产生负面影响的情况?(但请不要在此回答!) - Lars
3
这是一个非常有用的回答,我同意你的观点,TMP提供了一种非常好的纯函数式语言。虽然,constexpr也允许编写纯函数式代码,并提供了运行时和编译时变量之间的良好桥梁。然而,STL中没有像std::find这样被定义为constexpr的函数似乎人为地限制了可以使用STL编写的纯代码的种类。 - Benjamin Bannier

4

1
在你的例子中,f 在非 constexpr 变量 g 上调用了 operator++,因此不是有效的 constexpr 函数。g++(快照 2011-02-19)正确地抛出错误消息:“g 不是常量表达式”。 - Lars
@Lars:我已经多次阅读标准(草案3225)中的[dcl.constexpr]部分,但是没有找到任何阻止此函数成为“constexpr”的内容,因为文本“expression是潜在常量表达式”被删除了。 - Ben Voigt
“Example”链接已失效,ideone显示“未找到解决方案”。 - Alexander Malakhov
@AlexanderMalakhov:向ideone报告。他们的常见问题保证代码“永远”可用:https://ideone.com/faq 我不再使用该网站,因为他们会在未经警告且违反自己条款的情况下删除内容。 - Ben Voigt
2
我并不是想说这是你的错 :) 但是也许你还记得那段代码,能否在这里重现一下呢? - Alexander Malakhov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接