web-dev-qa-db-fra.com

Récursion de la queue en C++

Est-ce que quelqu'un peut me montrer une simple fonction récursive en C++?

Pourquoi la récursion de la queue est-elle meilleure, si c'est le cas?

Quels autres types de récursion existe-t-il en plus de la récursion de la queue?

53
neuromancer

Une fonction récursive simple de la queue:

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

La récursion de la queue est essentiellement quand

  • il n'y a qu'un seul appel récursif
  • cet appel est la dernière déclaration de la fonction

Et ce n'est pas "meilleur", sauf dans le sens où un bon compilateur peut supprimer la récursion en la transformant en boucle. Cela peut être plus rapide et fera certainement économiser sur l'utilisation de la pile. Le compilateur GCC peut faire cette optimisation.

58
anon

La récusion de queue en C++ a le même aspect que le langage C ou tout autre langage.

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

La récursion de queue (et l'appel de queue en général) nécessite d'effacer le cadre de pile de l'appelant avant d'exécuter l'appel de queue. Pour le programmeur, la récursion de la queue ressemble à une boucle, avec return réduit à fonctionner comme goto first_line;. Le compilateur doit toutefois détecter ce que vous faites. Sinon, il y aura toujours un cadre de pile supplémentaire. La plupart des compilateurs le supportent, mais écrire une boucle ou goto est généralement plus facile et moins risqué.

Les appels de fin non récursifs peuvent permettre la création de branches aléatoires (comme goto vers la première ligne d'une autre fonction), ce qui constitue une fonction plus unique.

Notez qu'en C++, il ne peut y avoir aucun objet avec un destructeur non trivial dans la portée de l'instruction return. Le nettoyage de fin de fonction nécessiterait que l'appelé revienne à l'appelant, éliminant ainsi l'appel final.

Notez également (dans toutes les langues) que la récursion finale nécessite que l'ensemble de l'état de l'algorithme soit transmis à travers la liste des arguments de la fonction à chaque étape. (Cela ressort clairement de l'exigence selon laquelle le cadre de pile de la fonction doit être éliminé avant le prochain appel… vous ne pouvez enregistrer aucune donnée dans des variables locales.) En outre, aucune opération ne peut être appliquée à la valeur de retour de la fonction avant qu'elle ne soit renvoyée à la fin .

int factorial( int n, int acc = 1 ) {
    if ( n == 0 ) return acc;
    else return factorial( n-1, acc * n );
}
40
Potatoswatter

La récursion de la queue est un cas particulier d'appel de la queue. Un appel final est l'endroit où le compilateur peut voir qu'il n'y a pas d'opérations à effectuer au retour d'une fonction appelée - il s'agit essentiellement de transformer le retour de la fonction appelée en propre. Le compilateur peut souvent effectuer quelques opérations de correction de pile, puis sauter (plutôt que d'appeler) à l'adresse de la première instruction de la fonction appelée .

Outre l’élimination de certains appels en retour, l’un des avantages de cette solution est de réduire l’utilisation de la pile. Sur certaines plates-formes ou dans le code du système d'exploitation, la pile peut être assez limitée et sur des machines avancées telles que les processeurs x86 de nos ordinateurs de bureau, réduire l'utilisation de la pile de manière à améliorer les performances du cache de données.

La récursion de queue est où la fonction appelée est la même que la fonction appelante. Cela peut être transformé en boucles, ce qui est exactement le même que l'optimisation du saut d'appel mentionné ci-dessus. Comme il s'agit de la même fonction (appelant et appelant), il y a moins de corrections à effectuer sur la pile avant le saut.

Ce qui suit montre une manière courante de faire un appel récursif qui serait plus difficile à transformer en compilateur pour un compilateur:

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

C’est assez simple pour que de nombreux compilateurs puissent le comprendre de toute façon, mais comme vous pouvez le constater, il faut ajouter un nombre après que le retour de la somme appelée renvoie un nombre. Il n’est donc pas possible d’optimiser un simple appel final.

Si vous avez fait:

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);
}

Vous seriez en mesure de tirer parti des appels dans les deux fonctions en tant qu'appels de retour. Ici, le travail principal de la fonction sum consiste à déplacer une valeur et à effacer un registre ou une position de pile. Sum_helper fait tout le calcul.

Puisque vous avez mentionné le C++ dans votre question, je mentionnerai certaines choses spéciales à ce sujet… .. C++ vous cache certaines choses que le C ne cache pas. Parmi ces destructeurs, l'essentiel sera l'optimisation de l'appel final.

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

Dans cet exemple, l’appel à baz n’est pas vraiment un appel final, car z doit être détruit après le retour de baz. Je pense que les règles de C++ peuvent rendre l’optimisation plus difficile même dans les cas où la variable n’est pas nécessaire pour la durée de l’appel, telles que:

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

z devra peut-être être détruit après le retour de qwerty ici.

Une autre chose serait la conversion de type implicite, ce qui peut arriver aussi en C, mais peut être plus compliqué et commun en 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);
}

Ici l'appel de sum à sum_helper n'est pas un appel final, car sum_helper renvoie un double et sum devra convertir cela en un entier.

En C++, il est assez courant de renvoyer une référence d'objet pouvant avoir toutes sortes d'interprétations différentes, chacune pouvant être un type de conversion différent, Par exemple:

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

Ici, il y a un appel à cout.operator << comme dernière instruction. cout retournera une référence à lui-même (c'est pourquoi vous pouvez enchaîner beaucoup de choses dans une liste séparée par <<), que vous forcerez alors à évaluer comme un bool, ce qui finira par appeler une autre des méthodes de cout, l'opérateur bool ( ). Ce cout.operator bool () pourrait être appelé comme un appel de queue dans ce cas, mais l'opérateur << ne pourrait pas.

MODIFIER:

Il est à noter qu'une des principales raisons de l'optimisation de l'appel final en C est que le compilateur sait que la fonction appelée stockera sa valeur de retour au même endroit que la fonction appelante pour s'assurer que sa valeur de retour est stocké dans.

26
nategoose

La récursion de queue n'existe pas vraiment au niveau du compilateur en C++. 

Bien que vous puissiez écrire des programmes qui utilisent la récursion de queue, vous ne bénéficiez pas des avantages hérités de la récursivité de queue mis en œuvre par les compilateurs/interprètes/langages. Par exemple, Scheme prend en charge une optimisation de la récursion de la queue afin de changer la récursivité en itération. Cela rend plus rapide et invulnérable d'empiler les débordements. C++ n'a pas une telle chose. (moins aucun compilateur que j'ai vu)

Apparemment, les optimisations de récursion de queue existent dans MSVC++ et GCC. Voir cette question pour plus de détails.

1
Earlz

Wikipedia a un article décent sur la récursion de la queue . Fondamentalement, la récursion de la queue est préférable à la récursion régulière car il est trivial de l'optimiser dans une boucle itérative, et les boucles itératives sont généralement plus efficaces que les appels de fonction récursifs. Ceci est particulièrement important dans les langages fonctionnels où vous n'avez pas de boucles.

Pour le C++, il est toujours utile de pouvoir écrire vos boucles récursives avec la récursion de la queue car elles peuvent être mieux optimisées, mais dans ce cas, vous pouvez généralement le faire de manière itérative en premier lieu, de sorte que le gain n’est pas aussi important qu’il le serait. être dans un langage fonctionnel.

0
Jonathan M Davis

La récursion de la queue est une astuce pour faire face à deux problèmes en même temps. La première consiste à exécuter une boucle lorsqu'il est difficile de connaître le nombre d'itérations à effectuer. 

Bien que cela puisse être résolu avec une simple récursion, le deuxième problème est celui du débordement de pile dû à l'appel récursif exécuté trop souvent. L'appel de queue est la solution, lorsqu'il est accompagné d'une technique de "calculer et transporter". 

Dans Basic CS, vous apprenez qu'un algorithme informatique doit avoir une invariante et une condition de fin. C'est la base pour construire la récursion de la queue.

  1. Tout le calcul se passe dans l'argument passant.
  2. Tous les résultats doivent être transmis aux appels de fonction.
  3. Le dernier appel est le dernier appel et se produit à la fin.

Pour le dire simplement,aucun calcul ne doit se produire sur la valeur de retour de votre fonction.

Prenons par exemple le calcul d'une puissance de 10, ce qui est trivial et peut être écrit par une boucle.

Devrait ressembler à quelque chose comme

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

Cela donne une exécution, par exemple 4:

ret, p, res

-, 4,1

-, 3,10

- 2 100

-, 1 000

-, 0,10000 

10000, -, -

Il est clair que le compilateur doit simplement copier des valeurs sans changer le pointeur de la pile et lorsque l'appel final se produit, il ne fait que renvoyer le résultat.

La récursion de la queue est très importante car elle peut fournir des évaluations prêtes à l'emploi, par exemple Ce qui précède peut être fait pour être.

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;
}

};

ceci peut être utilisé comme powc10<10>()() pour calculer la 10ème puissance au moment de la compilation. 

La plupart des compilateurs ont une limite d'appels imbriqués. Evidemment, il n'y a pas de boucle de méta-programmation, il faut donc utiliser la récursivité.

0
g24l