Подтвердить что ты не робот

Почему он плохо сформирован, чтобы иметь многострочные функции constexpr?

В соответствии с Обобщенные константные выражения-версия 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.

4b9b3361

Ответ 1

Причина в том, что у компилятора есть много дел, не будучи полноправным интерпретатором, способным оценивать произвольный код на С++.

Если они придерживаются единичных выражений, они ограничивают число случаев, которые нужно рассматривать резко. Как бы то ни было, это упрощает многое, что нет точек с запятой в частности.

Каждый раз, когда встречается a ;, это означает, что компилятор должен иметь дело с побочными эффектами. Это означает, что в предыдущем утверждении было изменено некоторое локальное состояние, на которое полагается следующее утверждение. Это означает, что оцениваемый код - это не просто серия простых операций, каждый из которых принимает в качестве входных данных предыдущий выходной сигнал, но также требует доступа к памяти, о чем гораздо труднее рассуждать.

В двух словах:

7 * 2 + 4 * 3

просто вычислить. Вы можете построить дерево синтаксиса, которое выглядит так:

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

и компилятор может просто пересечь это дерево, выполняя эти примитивные операции на каждом node, а корень node неявно является возвращаемым значением выражения.

Если бы мы должны были написать одно и то же вычисление с использованием нескольких строк, мы могли бы сделать это следующим образом:

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;

что гораздо труднее интерпретировать. Нам нужно обрабатывать чтение и запись в памяти, и мы должны обрабатывать операторы return. Наше синтаксическое дерево стало намного сложнее. Нам нужно обрабатывать объявления переменных. Нам нужно обрабатывать операторы, которые не имеют возвращаемого значения (например, цикл или запись в память), но которые просто меняют какую-то память. Какая память? Где? Что делать, если он случайно перезаписывает часть собственной памяти компилятора? Что делать, если это segfaults?

Даже без всяких противных "what-if" код, который должен интерпретировать компилятор, стал намного сложнее. Дерево синтаксиса теперь может выглядеть примерно так: (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 была выполнена ранее в этом же месте.

Ответ 2

На всякий случай здесь возникает путаница, вы знаете, что constexpr функции/выражения оцениваются во время компиляции. В этом нет проблемы с производительностью исполнения.

Зная это, причина, по которой они допускают только одиночные операторы возврата в функциях constexpr, заключается в том, что разработчикам компилятора не нужно писать виртуальную машину для вычисления постоянного значения.

Меня беспокоит проблема QoI. Интересно, будут ли разработчики компилятора достаточно умны для выполнения memoization?

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

Без memoization вышеуказанная функция имеет сложность O (2 n), которая, конечно же, не то, что я хотел бы чувствовать, даже во время компиляции.

Ответ 3

Как я понимаю, они максимально упростили это, чтобы не усложнять язык (на самом деле, похоже, я помню время, когда рекурсивные вызовы не были разрешены, но это уже не так). Обоснование заключается в том, что гораздо легче ослабить правила в будущих стандартах, чем ограничивать их.

Ответ 4

EDIT: проигнорировать этот ответ. Указанный документ устарел. Стандарт позволит ограничить рекурсию (см. Комментарии).

Обе формы являются незаконными. Рекурсия не допускается в 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 за то, что я пробежал свою память с вашим комментарием по моему другому ответу). Вместо того, чтобы налагать произвольные ограничения на рекурсию, дизайнеры посчитали проще просто сказать "Нет".

Ответ 5

Вероятно, он плохо сформировался, потому что его слишком сложно реализовать. Аналогичное решение было принято в первой версии стандарта в отношении замыканий на функции членов (т.е. Возможность пропускать obj.func в качестве вызываемой функции). Возможно, более поздняя версия стандарта предложит больше широты.