Хвостовая рекурсия в C ++ - PullRequest
58 голосов
/ 22 апреля 2010

Может ли кто-нибудь показать мне простую хвостовую рекурсию в C ++?

Почему хвостовая рекурсия лучше, если она вообще есть?

Какие существуют другие виды рекурсии, кроме хвостовой рекурсии?

Ответы [ 6 ]

61 голосов
/ 22 апреля 2010

Простая хвостовая рекурсивная функция:

unsigned int f( unsigned int a ) {
   if ( a == 0 ) {
      return a;
   }
   return f( a - 1 );   // tail recursion
}

Хвостовая рекурсия в основном когда:

  • есть только один рекурсивный вызов
  • этот вызов является последним оператором в функции

И это не «лучше», за исключением того, что хороший компилятор может удалить рекурсию, превратив ее в цикл Это может быть быстрее и, безусловно, сэкономит на использовании стека. Компилятор GCC может выполнить эту оптимизацию.

41 голосов
/ 22 апреля 2010

Отказ от хвоста в C ++ выглядит так же, как C или любой другой язык.

void countdown( int count ) {
    if ( count ) return countdown( count - 1 );
}

Хвостовая рекурсия (и хвостовой вызов в целом) требует очистки фрейма стека вызывающей стороны перед выполнением хвостового вызова. Для программиста хвостовая рекурсия похожа на цикл, с return сокращено до работы как goto first_line;. Однако компилятору необходимо определить, что вы делаете, и если это не так, все равно будет дополнительный кадр стека. Большинство компиляторов поддерживают это, но написание цикла или goto обычно проще и менее рискованно.

Нерекурсивные оконечные вызовы могут включать случайное ветвление (например, goto до первой строки какой-либо другой функции), что является более уникальным средством.

Обратите внимание, что в C ++ не может быть объектов с нетривиальным деструктором в области действия оператора return. Очистка конца функции потребует, чтобы вызываемый абонент вернулся обратно к вызывающему, что исключило бы хвостовой вызов.

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

int factorial( int n, int acc = 1 ) {
    if ( n == 0 ) return acc;
    else return factorial( n-1, acc * n );
}
28 голосов
/ 22 апреля 2010

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

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

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

Ниже показан распространенный способ сделать рекурсивный вызов, который компилятору будет сложнее превратить в цикл:

int sum(int a[], unsigned len) {
     if (len==0) {
         return 0;
     }
     return a[0] + sum(a+1,len-1);
}

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

Если вы сделали:

static int sum_helper(int acc, unsigned len, int a[]) {
     if (len == 0) {
        return acc;
     }
     return sum_helper(acc+a[0], len-1, a+1);
}
int sum(int a[], unsigned len) {
     return sum_helper(0, len, a);
}

Вы сможете воспользоваться вызовами в обеих функциях, которые являются хвостовыми вызовами. Здесь основная задача функции sum - переместить значение и очистить регистр или позицию стека. Sum_helper выполняет всю математику.

Поскольку вы упомянули C ++ в своем вопросе, я упомяну некоторые особые вещи по этому поводу. C ++ скрывает от вас некоторые вещи, которые C не делает. Из этих деструкторов это главное, что встанет на пути оптимизации хвостового вызова.

int boo(yin * x, yang *y) {
    dharma z = x->foo() + y->bar();
    return z.baz();
}

В этом примере вызов baz на самом деле не является хвостовым вызовом, поскольку после возврата из baz необходимо уничтожить z. Я считаю, что правила C ++ могут усложнить оптимизацию даже в тех случаях, когда переменная не нужна во время вызова, например:

int boo(yin * x, yang *y) {
    dharma z = x->foo() + y->bar();
    int u = z.baz();
    return qwerty(u);
}

z может быть уничтожено после возвращения из qwerty здесь.

Другая вещь - это неявное преобразование типов, которое также может происходить в C, но может быть более сложным и распространенным в C ++. Например:

static double sum_helper(double acc, unsigned len, double a[]) {
     if (len == 0) {
        return acc;
     }
     return sum_helper(acc+a[0], len-1, a+1);
}
int sum(double a[], unsigned len) {
     return sum_helper(0.0, len, a);
}

Здесь вызов sum для sum_helper не является хвостовым вызовом, потому что sum_helper возвращает значение типа double, и sum необходимо преобразовать его в int.

В C ++ довольно часто возвращают ссылку на объект, которая может иметь различные интерпретации, каждая из которых может быть различным преобразованием типа, Например:

bool write_it(int it) {
      return cout << it;
}

Здесь есть вызов cout.operator << в качестве последнего оператора. cout вернет ссылку на себя (именно поэтому вы можете связать много вещей в список, разделенный <<), который затем вынуждает быть оцененным как bool, который в итоге вызывает другой метод cout, оператор bool ( ). В этом случае этот cout.operator bool () может быть вызван как оконечный вызов, но оператор << не может. </p>

EDIT:

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

2 голосов
/ 30 ноября 2015

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

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

В базовой CS вы узнаете, что компьютерный алгоритм должен иметь инвариант и условие завершения. Это основа для построения хвостовой рекурсии.

  1. Все вычисления происходят при передаче аргумента.
  2. Все результаты должны быть переданы на вызовы функций.
  3. Конечный вызов является последним вызовом и происходит при завершении.

Проще говоря, не должно выполняться вычислений для возвращаемого значения вашей функции .

Возьмем, например, вычисление степени 10, которое тривиально и может быть записано в цикле.

Должно выглядеть примерно так:

template<typename T> T pow10(T const p, T const res =1)
{
return p ? res: pow10(--p,10*res);
}

Это дает исполнение, например, 4:

RET, р, Рез

-, 4,1

-, 3,10

-, 2100

-, 1,1000

-, 0,10000

10000, -, -

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

Хвостовая рекурсия очень важна, потому что она может обеспечить готовые оценки времени компиляции, например, Выше может быть сделано.

template<int N,int R=1> struct powc10
{
int operator()() const
{
return  powc10<N-1, 10*R>()();
}
};

template<int R> struct powc10<0,R>
{

int operator()() const
{
return  R;
}

};

это можно использовать как powc10<10>()() для вычисления 10-й степени во время компиляции.

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

1 голос
/ 22 апреля 2010

Хвостовая рекурсия на самом деле не существует на уровне компилятора в C ++.

Хотя вы можете писать программы, использующие хвостовую рекурсию, вы не получаете преимуществ наследования хвостовой рекурсии, реализуемых поддержкой компиляторов / интерпретаторов / языков. Например, Scheme поддерживает оптимизацию хвостовой рекурсии, так что она в основном преобразует рекурсию в итерацию. Это делает его более быстрым и неуязвимым для переполнения стека. C ++ не имеет такой вещи. (хотя бы не тот компилятор, который я видел)

Очевидно, что оптимизация хвостовой рекурсии существует как в MSVC ++, так и в GCC. См. этот вопрос для деталей.

0 голосов
/ 22 апреля 2010

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

Для C ++ все еще хорошо, если вы можете написать свои рекурсивные циклы с хвостовой рекурсией, поскольку они могут быть лучше оптимизированы, но в таких случаях вы, как правило, можете просто делать это итеративно, во-первых, так что усиление не так велико как это было бы на функциональном языке.

...