25

根据广义常量表达式——修订版 5,以下内容是非法的。

constexpr int g(int n) // error: body not just ‘‘return expr’’
{
    int r = n;
    while (--n > 1) r *= n;
    return r;
}

这是因为所有 'constexpr' 函数都必须采用{ return expression; }. 我看不出有任何理由需要这样做。

在我看来,唯一真正需要的是不读取/写入外部状态信息,并且传入的参数也是“constexpr”语句。这意味着对具有相同参数的函数的任何调用都将返回相同的结果,因此可以在编译时“知道”。

我的主要问题是它似乎只是迫使你做真正迂回的循环形式,并希望编译器对其进行优化,以便它对于非 constexpr 调用同样快。

constexpr要为上面的例子写一个有效的,你可以这样做:

constexpr int g(int n) // error: body not just ‘‘return expr’’
{
    return (n <= 1) ? n : (n * g(n-1));
}

但这更难理解,当您使用违反const-expr.

4

5 回答 5

18

原因是编译器已经有很多工作要做,它还不是一个成熟的解释器,能够评估任意 C++ 代码。

如果他们坚持使用单一表达式,他们会极大地限制要考虑的案例数量。松散地说,它简化了很多事情,特别是没有分号。

每次;遇到 a 都意味着编译器必须处理副作用。这意味着在上一个语句中更改了某些本地状态,以下语句将依赖该状态。这意味着被评估的代码不再只是一系列简单的操作,每个操作都将前一个操作的输出作为其输入,而且还需要访问内存,这很难推理。

简而言之,这是:

7 * 2 + 4 * 3

计算简单。您可以构建如下所示的语法树:

   +
  /\
 /  \
 *   *
/\  /\
7 2 4 3

并且编译器可以简单地遍历这棵树,在每个节点上执行这些原始操作,并且根节点隐含地是表达式的返回值。

如果我们要使用多行编写相同的计算,我们可以这样做:

int i0 = 7;
int i1 = 2;
int i2 = 4;
int i3 = 3;

int i4 = i0 * i1;
int i5 = i2 * i3;
int i6 = i4 + i5;
return i6;

这很难解释。我们需要处理内存读取和写入,我们必须处理返回语句。我们的语法树变得更加复杂。我们需要处理变量声明。我们需要处理没有返回值的语句(例如,循环或内存写入),但只是在某处修改了一些内存。哪个内存?在哪里?如果它不小心覆盖了一些编译器自己的内存怎么办?如果出现段错误怎么办?

即使没有所有令人讨厌的“假设”,编译器必须解释的代码也变得更加复杂。语法树现在可能看起来像这样:(LD并且ST分别是加载和存储操作)

    ;    
    /\
   ST \
   /\  \
  i0 3  \
        ;
       /\
      ST \
      /\  \
     i1 4  \
           ;
          /\
         ST \
         / \ \
       i2  2  \
              ;
             /\
            ST \
            /\  \
           i3 7  \
                 ;
                /\
               ST \
               /\  \
              i4 *  \
                 /\  \
               LD LD  \
                |  |   \
                i0 i1   \
                        ;
                       /\
                      ST \
                      /\  \
                     i5 *  \
                        /\  \
                       LD LD \
                        |  |  \
                        i2 i3  \
                               ;
                              /\
                             ST \
                             /\  \
                            i6 +  \
                               /\  \
                              LD LD \
                               |  |  \
                               i4 i5  \
                                      LD
                                       |
                                       i6

它不仅看起来更复杂,而且现在还需要状态。以前,每个子树都可以单独解释。现在,它们都依赖于程序的其余部分。LD 叶操作之一没有意义,除非它被放置在树中,以便ST先前在同一位置执行操作

于 2010-07-12T10:02:54.803 回答
5

以防万一这里有任何混淆,您知道constexpr函数/表达式是在compile-time评估的。不涉及运行时性能问题。

知道了这一点,它们只允许在constexpr函数中使用单个返回语句的原因是编译器实现者不需要编写虚拟机来计算常量值。

我担心这方面的 QoI 问题。我想知道编译器实现者是否足够聪明来执行记忆?

constexpr fib(int n) { return < 2 ? 1 : fib(n-1) + fib(n-2); }

如果没有记忆,上述函数具有O(2 n )复杂性,这当然不是我想要的,即使在编译时也是如此。

于 2010-07-12T07:14:40.210 回答
2

据我了解,他们尽可能保持简单,以免语言复杂化(事实上,我似乎记得有一次不允许递归调用,但现在不再如此)。理由是在未来的标准中放宽规则要比限制规则容易得多。

于 2010-07-12T07:08:32.557 回答
2

编辑:忽略这个答案。引用的论文已过期。该标准将允许有限的递归(见评论)。

这两种形式都是非法的。constexpr 函数中不允许递归,因为在定义之前不能调用 constexpr 函数。OP提供的链接明确说明了这一点:

constexpr int twice(int x);
enum { bufsz = twice(256) }; // error: twice() isn’t (yet) defined

constexpr int fac(int x)
{ return x > 2 ? x * fac(x - 1) : 1; } // error: fac() not defined
                                       // before use

再往下几行:

常量表达式函数只能调用先前定义的常量表达式函数的要求确保我们不会遇到任何与递归相关的问题。

...

我们(仍然)禁止常量表达式中所有形式的递归。

如果没有这些限制,您就会陷入停顿问题(感谢@Grant 用您对我的其他答案的评论来唤起我的记忆)。设计者没有强加任意递归限制,而是认为说“不”更简单。

于 2010-07-13T02:34:46.587 回答
0

它可能是不正确的,因为它太难实现了。在标准的第一个版本中,关于成员函数闭包(即,能够obj.func作为可调用函数传递)做出了类似的决定。也许稍后对该标准的修订将提供更大的自由度。

于 2010-07-12T06:26:52.113 回答