On ne fait pas de la magie

«Toute technologie suffisamment avancée est indiscernable de la magie.» Arthur C. Clarke

Fonctions lambda récursives en C++23 avec le deducing this

Comme à chaque fois que je prépare une nouvelle session de formation C++ 20 et 23, j’ai refait le tour des exemples que je présente. L’un d’eux concerne une des utilisations possibles du « deducing this », qu’il me parait intéressant de vous présenter.

Dans les nouvelles fonctionnalités de C++23, il en y a une qui est simple à mettre en œuvre et qui permet de résoudre plusieurs problèmes différents : le « deducing this » ou « explicit object parameters ».

De quoi s’agit il ? En C++23 il est possible d’indiquer dans la signature d’une fonction membre un paramètre qui désigne l’instance utilisée pour effectuer l’appel de la fonction.

Un petit rappel s’impose : en C++, comme dans un grand nombre de langages orientés objet, une fonction membre a un paramètre caché, fourni implicitement, qui est l’objet sur lequel la fonction est appelée.

Depuis C++23, l’attribut this peut être utilisé pour qualifier le premier paramètre d’une fonction membre qui désigne alors explicitement l’objet d’appel de la fonction.

Dans la définition de classe Chercheur, le paramètre chercheur de la fonction membre getPersonne représente explicitement l’instance d’appel.

struct Personne
{
	string nom;
	string url;
};
class Chercheur {
    Personne p;
public:
    Chercheur(Personne p) : p{p} {}
#if __cplusplus > 202002L // a partir de C++ 23
    template <typename T> auto && getPersonne(this T && chercheur) { return chercheur.p; }
#else // avant C++23
    Personne  getPersonne() { return p; }
    Personne const getPersonne() const { return p; }
#endif
};   

Une utilisation courante est d’éviter la duplication de code. Dans l’exemple ci dessus, nous avons une seule fonction à définir (ligne 13) avec C++23 pour traiter le cas de l’appel avec une instance constante ou non. au lieu de deux fonctions nécessaires dans les versions antérieures (lignes 15 et 16). (On pourrait même monter à quatre fonctions si on voulait traiter explicitement les cas des références et RValues).

La fonctionnalité utilisée ici est la définition du type de this, permettant de spécifier le type de passage (par référence, par valeur, …). Cette fonctionnalité permet d’utiliser l’explicit object paramater pour différents cas d’usage : polymorphisme statique (CRTP -Curiously Recurring Template pattern), perfect forwarding dans un lambda, SFINAE spécifiques, …

Une autre fonctionnalité va consister à utiliser le fait que dans une fonction lambda, this représente la fonction lambda elle même. Il devient donc possible de créer aisément une fonction lambda récursive. Avant C++23, il existait des moyens détournés de le faire, par exemple en utilisant une instance de function, mais avec des performances très dégradées.

Voici un exemple d’une fonction lambda factorielle récursive ;

#include <iostream>
using namespace std;

int main()
{
	int fact = [](this auto&& f, auto n) -> int {
		return (n > 0) ? n*f(n - 1):1; } (5);
	cout << fact << endl;
	return 0;
}

On a simplement accès, au travers de la variable f, à la fonction lambda elle même. On peut donc l’utiliser pour exprimer que n! = n * (n-1)! et 0! = 1 comme on le ferait pour écrire une fonction factorielle récursive classique en C++.

Les fonctions récursives sont souvent employées pour des calculs mathématiques et des explorations d’arbres ou de structures hiérarchiques. En C++ 23, il devient possible d’utiliser des fonctions lambda pour les écrire, de façon plus simple. Voici un exemple didactique de parcours récursif d’un répertoire :

#include <iostream>
#include <filesystem>
#include <format>

using namespace  std::filesystem;
using namespace std;

int main() {
	[](this auto&& parcours, const path& rep, int depth = 0)  {
		if (!exists(rep) || !is_directory(rep)) {
			return;
		}
		string indent(depth * 2, ' ');
		cout << format("{}[Repertoire]  {}\n", indent, rep.filename().string());
		for (const auto& entree : directory_iterator(rep)) {
			if (entree.is_directory()) {				
				parcours(entree.path(), depth + 1);
			}
			else {
				cout << format("{} {} ({} octets)\n", indent, entree.path().filename().string(), file_size(entree));
			}
		}
	} (".");

	return 0;
}

Cet exemple peut être exécuté ici.

Un développeur C++ dispose maintenant d’un moyen élégant et performant pour écrire des fonctions lambda récursives. Cette amélioration fait partie des nombreuses avancées des normes C++20 et C++23 que je suis heureux de transmettre lors de formations ou d’assistances que je réalise.

Commentaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *