Это может помочь вам подумать об этом с точки зрения того, как на самом деле реализована оптимизация хвостового вызова.Конечно, это не часть определения, но оно мотивирует определение.
Обычно при вызове функции вызывающий код сохраняет в стеке любые значения регистров, которые понадобятся ему позже, в стеке.Также будет храниться обратный адрес с указанием следующей инструкции после вызова.Он будет делать все, что ему нужно, чтобы убедиться, что указатель стека правильно настроен для вызываемого.Затем он перейдет к целевому адресу [*] (в этом случае та же функция).По возвращении он знает, что возвращаемое значение находится в месте, указанном соглашением о вызовах (регистр или слот стека).
Для хвостового вызова вызывающая сторона не делает этого.Он игнорирует любые значения регистров, потому что знает, что они не понадобятся позже.Он устанавливает указатель стека так, что вызываемый будет использовать тот же стек, что и вызывающий, и он не устанавливает себя в качестве адреса возврата, он просто переходит на целевой адрес.Таким образом, вызываемый объект перезапишет ту же область стека, он поместит свое возвращаемое значение в то же место, в котором вызывающий объект поместил бы свое возвращаемое значение, и когда он вернется, он не вернется к своему вызывающему, но вернет к своему вызывающему.caller.
Следовательно, неофициально, функция является хвостовой рекурсивной, когда возможна оптимизация хвостового вызова, и когда целью хвостового вызова является сама функция.Эффект более или менее такой же, как если бы функция содержала цикл, и вместо вызова самого себя, вызов tail переходит к началу цикла.Это означает, что не должно быть никаких переменных, необходимых после вызова (и, действительно, никакой «работы для выполнения», что в языке, подобном C ++, означает, что ничего не должно быть уничтожено), и вызывающее лицо должно вернуть возвращаемое значение хвостового вызова.
Это все для простой / тривиальной хвостовой рекурсии.Существуют преобразования, которые можно использовать для создания чего-то хвостового рекурсивного, чего еще нет, например, для введения дополнительных параметров, которые хранят некоторую информацию, используемую «самым нижним» уровнем рекурсии, для выполнения работы, которая в противном случае была бы выполнена навыход".Так, например:
int triangle(int n) {
if (n == 0) return 0;
return n + triangle(n-1);
}
может быть сделан хвост-рекурсивным, либо программистом, либо автоматически с помощью достаточно умного компилятора, например:
int triangle(int n, int accumulator = 0) {
if (n == 0) return accumulator;
return triangle(n-1, accumulator + n);
}
Следовательно, прежняя функция можетбыть описанным как "хвостовая рекурсия" кем-то, кто говорит о достаточно умном языке / компиляторе.Будьте готовы к этому варианту использования.
[*] Сохранение адреса возврата, перемещение указателя стека и переход, может или не может быть заключено в один код операции архитектурой, но даже если это не так, как правилочто происходит.