Fonctions lambda récursives en C ++ 11


143

Je suis nouveau sur C ++ 11. J'écris la fonction lambda récursive suivante, mais elle ne se compile pas.

sum.cpp

#include <iostream>
#include <functional>

auto term = [](int a)->int {
  return a*a;
};

auto next = [](int a)->int {
  return ++a;
};

auto sum = [term,next,&sum](int a, int b)mutable ->int {
  if(a>b)
    return 0;
  else
    return term(a) + sum(next(a),b);
};

int main(){
  std::cout<<sum(1,10)<<std::endl;
  return 0;
}

erreur de compilation:

vimal @ linux-718q: ~ / Study / 09C ++ / c ++ 0x / lambda> g ++ -std = c ++ 0x sum.cpp

sum.cpp: Dans la fonction lambda: sum.cpp: 18: 36: erreur: ' ((<lambda(int, int)>*)this)-><lambda(int, int)>::sum' ne peut pas être utilisé comme fonction

version gcc

gcc version 4.5.0 20091231 (expérimental) (GCC)

Mais si je change la déclaration de sum()comme ci-dessous, cela fonctionne:

std::function<int(int,int)> sum = [term,next,&sum](int a, int b)->int {
   if(a>b)
     return 0;
   else
     return term(a) + sum(next(a),b);
};

Quelqu'un pourrait-il s'il vous plaît jeter la lumière là-dessus?


Cela pourrait-il être des déclarations statiques ou implicitement dynamiques?
Hamish Grubijan

3
Que fait le mutablemot - clé là-bas?
Bravo et hth. - Alf

La capture de variables avec une durée de stockage non automatique n'est pas autorisée. Vous devriez le faire de cette façon: chat.stackoverflow.com/transcript/message/39298544#39298544
Euri Pinhollow

Juste un FYI, dans votre deuxième extrait de code, votre lambda est trop détaillé, considérez ce changement:std::function<int(int,int)> sum = [&](int a, int b) {
armanali

Réponses:


189

Pensez à la différence entre la version automatique et la version de type entièrement spécifiée. Le mot clé auto déduit son type de tout ce avec quoi il est initialisé, mais ce avec quoi vous l'initialisez doit savoir quel est son type (dans ce cas, la fermeture lambda doit connaître les types qu'elle capture). Quelque chose d'un problème de poule et d'oeuf.

D'un autre côté, le type d'un objet fonction entièrement spécifié n'a pas besoin de "savoir" quoi que ce soit sur ce qui lui est assigné, et ainsi la fermeture du lambda peut également être pleinement informée des types qu'il capture.

Considérez cette légère modification de votre code et cela peut avoir plus de sens:

std::function<int(int,int)> sum;
sum = [term,next,&sum](int a, int b)->int {
if(a>b)
    return 0;
else
    return term(a) + sum(next(a),b);
};

De toute évidence, cela ne fonctionnerait pas avec l' automobile . Les fonctions lambda récursives fonctionnent parfaitement bien (du moins elles le font dans MSVC, où j'ai de l'expérience avec elles), c'est juste qu'elles ne sont pas vraiment compatibles avec l'inférence de type.


3
Je ne suis pas d'accord avec ça. Le type du lambda est bien connu dès que le corps de la fonction est entré - il n'y a aucune raison pour qu'il ne soit pas déduit d'ici là.
Puppy

16
@DeadMG mais la spécification interdit de faire référence à la autovariable dans l'initialiseur de celui-ci. le type de la variable automatique n'est pas encore connu lorsque l'initialiseur est en cours de traitement.
Johannes Schaub - litb

1
Vous vous demandez pourquoi cela n'est pas marqué comme «réponse», et que celui de Python est classé comme «réponse»?!
Ajay

1
@Puppy: Dans le cas d'une capture implicite, cependant, pour plus d'efficacité, seules les variables référencées sont réellement capturées, le corps doit donc être analysé.
kec

Existe-t-il une interprétation valide pour sumautre que std::function<int(int, int)>, ou la spécification C ++ n'a-t-elle tout simplement pas pris la peine de l'inférer?
Mateen Ulhaq le

79

L'astuce consiste à alimenter l'implémentation lambda en tant que paramètre , et non par capture.

const auto sum = [term,next](int a, int b) {
  auto sum_impl=[term,next](int a,int b,auto& sum_ref) mutable {
    if(a>b){
      return 0;
    }
    return term(a) + sum_ref(next(a),b,sum_ref);
  };
  return sum_impl(a,b,sum_impl);
};

Tous les problèmes en informatique peuvent être résolus par un autre niveau d'indirection . J'ai trouvé cette astuce pour la première fois sur http://pedromelendez.com/blog/2015/07/16/recursive-lambdas-in-c14/

Il ne nécessite C ++ 14 alors que la question est en C ++ 11, mais peut - être intéressant pour la plupart.

Passer via std::functionest également possible, mais peut entraîner un code plus lent. Mais pas toujours. Jetez un œil aux réponses à std :: function vs template


Ce n'est pas seulement une particularité du C ++, c'est un mappage direct avec les mathématiques du calcul lambda. De Wikipedia :

Lambda calculus cannot express this as directly as some other notations:
all functions are anonymous in lambda calculus, so we can't refer to a
value which is yet to be defined, inside the lambda term defining that
same value. However, recursion can still be achieved by arranging for a
lambda expression to receive itself as its argument value

3
Cela semble bien pire que d'utiliser explicitement function<>. Je ne vois pas pourquoi quelqu'un le préférerait. Edit: C'est plus rapide apparemment.
Timmmm

17
c'est bien meilleur que std :: function pour 3 raisons: il ne nécessite pas d'effacement de type ou d'allocation de mémoire, il peut être constexpr et il fonctionne correctement avec les paramètres automatiques (modèles) / le type de retour
Ivan Sanz-Carasa

3
Vraisemblablement, cette solution a également l'avantage d'être copiable sans que la référence std :: function sorte du champ d'application?
Uri Granta

3
Hm, en essayant, GCC 8.1 (linux) s'est plaint: error: use of ‘[...]’ before deduction of ‘auto’- nécessaire de spécifier explicitement le type de retour (d'un autre côté, n'a pas besoin de mutable).
Aconcagua

@Aconcagua même ici avec Xcode10 et j'ai mis la norme C ++ à 17 même
IceFire

39

Avec C ++ 14, il est maintenant assez facile de faire un lambda récursif efficace sans avoir à subir la surcharge supplémentaire de std::function, en seulement quelques lignes de code (avec une petite modification de l'original pour empêcher l'utilisateur de prendre une copie accidentelle ):

template <class F>
struct y_combinator {
    F f; // the lambda will be stored here

    // a forwarding operator():
    template <class... Args>
    decltype(auto) operator()(Args&&... args) const {
        // we pass ourselves to f, then the arguments.
        // [edit: Barry] pass in std::ref(*this) instead of *this
        return f(std::ref(*this), std::forward<Args>(args)...);
    }
};

// helper function that deduces the type of the lambda:
template <class F>
y_combinator<std::decay_t<F>> make_y_combinator(F&& f) {
    return {std::forward<F>(f)};
}

avec lequel votre sumtentative initiale devient:

auto sum = make_y_combinator([term,next](auto sum, int a, int b) {
  if (a>b) {
    return 0;
  }
  else {
    return term(a) + sum(next(a),b);
  }
});

En C ++ 17, avec CTAD, nous pouvons ajouter un guide de déduction:

template <class F> y_combinator(F) -> y_combinator<F>;

Ce qui évite la nécessité de la fonction d'assistance. Nous pouvons simplement écrire y_combinator{[](auto self, ...){...}}directement.


En C ++ 20, avec CTAD pour les agrégats, le guide de déduction ne sera pas nécessaire.


C'est génial, mais on pourrait envisager std::forward<decltype(sum)>(sum)au lieu de sumsur la dernière ligne.
Johan Lundberg

@Johan Non, il n'y en a qu'un operator()donc il n'y a rien à gagner à transférersum
Barry

Oh, c'est vrai. Pas habitué à utiliser des références de transfert sans transfert.
Johan Lundberg

Le combinateur Y est certainement la voie à suivre. Mais vous devriez vraiment ajouter une non- constsurcharge au cas où l'objet-fonction fourni a un constopérateur non -appel. Et utilisez SFINAE et calculé noexceptpour les deux. De plus, il n'y a plus besoin de la fonction maker dans C ++ 17.
Deduplicator

2
@minex Oui, auto sumcopie ... mais il copie un reference_wrapper, ce qui revient à prendre une référence. Le faire une fois dans l'implémentation signifie qu'aucune des utilisations ne sera jamais copiée accidentellement.
Barry le

22

J'ai une autre solution, mais je ne travaille qu'avec des lambdas sans état:

void f()
{
    static int (*self)(int) = [](int i)->int { return i>0 ? self(i-1)*i : 1; };
    std::cout<<self(10);
}

L'astuce ici est que les lambdas peuvent accéder aux variables statiques et vous pouvez convertir celles sans état en pointeur de fonction.

Vous pouvez l'utiliser avec des lambdas standard:

void g()
{
    int sum;
    auto rec = [&sum](int i) -> int
    {
        static int (*inner)(int&, int) = [](int& _sum, int i)->int 
        {
            _sum += i;
            return i>0 ? inner(_sum, i-1)*i : 1; 
        };
        return inner(sum, i);
    };
}

Son travail dans GCC 4.7


3
Cela devrait avoir de meilleures performances que std :: function, donc +1 pour l'alternative. Mais vraiment, à ce stade, je me demande si l'utilisation de lambdas est la meilleure option;)
Antoine

Si vous avez un lambda sans état, vous pouvez également en faire une fonction complète.
Timmmm

1
@Timmmm Mais ensuite, vous perdez une partie de l'implémentation vers un mot extérieur, généralement les lambdas sont étroitement associés à la fonction parent (même sans capture). Si ce n'était pas le cas, vous ne devriez pas utiliser lambdas en premier lieu et utiliser les fonctions normales des foncteurs.
Yankes

10

Vous pouvez faire en sorte qu'une fonction lambda s'appelle elle-même de manière récursive. La seule chose que vous devez faire est de le référencer via un wrapper de fonction afin que le compilateur sache son type de retour et d'argument (vous ne pouvez pas capturer une variable - le lambda lui-même - qui n'a pas encore été définie) .

  function<int (int)> f;

  f = [&f](int x) {
    if (x == 0) return 0;
    return x + f(x-1);
  };

  printf("%d\n", f(10));

Faites très attention à ne pas sortir de la portée du wrapper f.


3
Mais, c'est identique à la réponse acceptée, et peut avoir une pénalité pour l'utilisation de la fonction std.
Johan Lundberg

9

Pour rendre lambda récursif sans utiliser de classes et de fonctions externes (comme std::functionou un combinateur à virgule fixe), on peut utiliser la construction suivante en C ++ 14 ( exemple en direct ):

#include <utility>
#include <list>
#include <memory>
#include <iostream>

int main()
{
    struct tree
    {
        int payload;
        std::list< tree > children = {}; // std::list of incomplete type is allowed
    };
    std::size_t indent = 0;
    // indication of result type here is essential
    const auto print = [&] (const auto & self, const tree & node) -> void
    {
        std::cout << std::string(indent, ' ') << node.payload << '\n';
        ++indent;
        for (const tree & t : node.children) {
            self(self, t);
        }
        --indent;
    };
    print(print, {1, {{2, {{8}}}, {3, {{5, {{7}}}, {6}}}, {4}}});
}

imprime:

1
 2
  8
 3
  5
   7
  6
 4

Remarque: le type de résultat lambda doit être spécifié explicitement.


6

J'ai exécuté un benchmark comparant une fonction récursive à une fonction lambda récursive en utilisant la std::function<>méthode de capture. Avec des optimisations complètes activées sur la version 4.1 de clang, la version lambda fonctionnait beaucoup plus lentement.

#include <iostream>
#include <functional>
#include <chrono>

uint64_t sum1(int n) {
  return (n <= 1) ? 1 : n + sum1(n - 1);
}

std::function<uint64_t(int)> sum2 = [&] (int n) {
  return (n <= 1) ? 1 : n + sum2(n - 1);
};

auto const ITERATIONS = 10000;
auto const DEPTH = 100000;

template <class Func, class Input>
void benchmark(Func&& func, Input&& input) {
  auto t1 = std::chrono::high_resolution_clock::now();
  for (auto i = 0; i != ITERATIONS; ++i) {
    func(input);
  }
  auto t2 = std::chrono::high_resolution_clock::now();
  auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(t2-t1).count();
  std::cout << "Duration: " << duration << std::endl;
}

int main() {
  benchmark(sum1, DEPTH);
  benchmark(sum2, DEPTH);
}

Produit des résultats:

Duration: 0 // regular function
Duration: 4027 // lambda function

(Remarque: j'ai également confirmé avec une version qui prenait les entrées de cin, afin d'éliminer l'évaluation du temps de compilation)

Clang produit également un avertissement du compilateur:

main.cc:10:29: warning: variable 'sum2' is uninitialized when used within its own initialization [-Wuninitialized]

Ce qui est attendu et sûr, mais doit être noté.

C'est formidable d'avoir une solution dans nos ceintures d'outils, mais je pense que le langage aura besoin d'une meilleure façon de gérer ce cas si les performances doivent être comparables aux méthodes actuelles.

Remarque:

Comme l'a souligné un commentateur, il semble que la dernière version de VC ++ a trouvé un moyen d'optimiser cela au point de performances égales. Peut-être n'avons-nous pas besoin d'une meilleure façon de gérer cela, après tout (sauf pour le sucre syntaxique).

De plus, comme certains autres articles SO l'ont souligné ces dernières semaines, les performances en elles- std::function<>mêmes peuvent être la cause du ralentissement par rapport à la fonction d'appel directement, du moins lorsque la capture lambda est trop grande pour s'adapter à certaines std::functionutilisations de l' espace optimisé par la bibliothèque pour les petits foncteurs. (Je suppose qu'un peu comme les diverses optimisations de chaînes courtes?).


2
-1. Notez que la seule raison pour laquelle la version "lambda" prend plus de temps est parce que vous la liez à une fonction std ::, ce qui oblige l'opérateur () à appeler un appel virtuel, et cela prendrait évidemment plus de temps. En plus de cela, votre code, en mode version VS2012, a pris à peu près le même temps dans les deux cas.
Yam Marcovic

@YamMarcovic Quoi? C'est actuellement le seul moyen connu d'écrire un lambda récursif (c'était le but de l'exemple). Je suis très heureux de savoir que VS2012 a trouvé un moyen d'optimiser ce cas d'utilisation (bien qu'il y ait eu plus de développements sur ce sujet récemment, apparemment si mon lambda en avait capturé plus, il n'aurait pas rentrer dans la std :: function small- optimisations du foncteur de mémoire ou autres).
mmocny

2
Reconnu. J'ai mal compris votre message. +1 alors. Gah, ne peut voter que si vous modifiez cette réponse. Alors, pourriez-vous insister un peu plus, comme dans le commentaire?
Yam Marcovic

1
@YamMarcovic terminé. J'apprécie votre volonté de fournir des commentaires et de les affiner si nécessaire. +1 à vous, bon monsieur.
mmocny

Le temps 0 signifie généralement "toute l'opération a été optimisée". Prendre une entrée de cin ne fait rien si le compilateur prouve que vous ne faites rien avec le résultat de votre calcul.
Yakk - Adam Nevraumont

1

Il s'agit d'une implémentation légèrement plus simple de l'opérateur de point de fixation qui rend un peu plus évident ce qui se passe.

#include <iostream>
#include <functional>

using namespace std;

template<typename T, typename... Args>
struct fixpoint
{
    typedef function<T(Args...)> effective_type;
    typedef function<T(const effective_type&, Args...)> function_type;

    function_type f_nonr;

    T operator()(Args... args) const
    {
        return f_nonr(*this, args...);
    }

    fixpoint(const function_type& p_f)
        : f_nonr(p_f)
    {
    }
};


int main()
{
    auto fib_nonr = [](const function<int(int)>& f, int n) -> int
    {
        return n < 2 ? n : f(n-1) + f(n-2);
    };

    auto fib = fixpoint<int,int>(fib_nonr);

    for (int i = 0; i < 6; ++i)
    {
        cout << fib(i) << '\n';
    }
}

Je pense que vous pourriez améliorer votre réponse (en termes de performances) si vous remplacez std::functionpar un pointeur de fonction (des cœurs, cela ne fonctionnera qu'avec une fonction normale et des lambdas sans état). Btw fib_nonrdevrait accepter fixpoint<int,int>, si vous utilisez std::functionsa nouvelle copie à partir de *this.
Yankes

1

Voici une version raffinée de la solution Y-combinator basée sur celle proposée par @Barry.

template <class F>
struct recursive {
  F f;
  template <class... Ts>
  decltype(auto) operator()(Ts&&... ts)  const { return f(std::ref(*this), std::forward<Ts>(ts)...); }

  template <class... Ts>
  decltype(auto) operator()(Ts&&... ts)  { return f(std::ref(*this), std::forward<Ts>(ts)...); }
};

template <class F> recursive(F) -> recursive<F>;
auto const rec = [](auto f){ return recursive{std::move(f)}; };

Pour l'utiliser, on pourrait faire ce qui suit

auto fib = rec([&](auto&& fib, int i) {
// implementation detail omitted.
});

Il est similaire au let recmot - clé dans OCaml, mais pas le même.


0

C ++ 14: Voici un ensemble générique récursif anonyme sans état / sans capture de lambdas qui renvoie tous les nombres de 1, 20

([](auto f, auto n, auto m) {
    f(f, n, m);
})(
    [](auto f, auto n, auto m) -> void
{
    cout << typeid(n).name() << el;
    cout << n << el;
    if (n<m)
        f(f, ++n, m);
},
    1, 20);

Si je comprends bien, cela utilise la solution du combinateur Y

Et voici la version somme (n, m)

auto sum = [](auto n, auto m) {
    return ([](auto f, auto n, auto m) {
        int res = f(f, n, m);
        return res;
    })(
        [](auto f, auto n, auto m) -> int
        {
            if (n > m)
                return 0;
            else {
                int sum = n + f(f, n + 1, m);
                return sum;
            }
        },
        n, m); };

auto result = sum(1, 10); //result == 55

-1

Voici la réponse finale pour le PO. Quoi qu'il en soit, Visual Studio 2010 ne prend pas en charge la capture de variables globales. Et vous n'avez pas besoin de les capturer car la variable globale est accessible globalement par define. La réponse suivante utilise plutôt une variable locale.

#include <functional>
#include <iostream>

template<typename T>
struct t2t
{
    typedef T t;
};

template<typename R, typename V1, typename V2>
struct fixpoint
{
    typedef std::function<R (V1, V2)> func_t;
    typedef std::function<func_t (func_t)> tfunc_t;
    typedef std::function<func_t (tfunc_t)> yfunc_t;

    class loopfunc_t {
    public:
        func_t operator()(loopfunc_t v)const {
            return func(v);
        }
        template<typename L>
        loopfunc_t(const L &l):func(l){}
        typedef V1 Parameter1_t;
        typedef V2 Parameter2_t;
    private:
        std::function<func_t (loopfunc_t)> func;
    };
    static yfunc_t fix;
};
template<typename R, typename V1, typename V2>
typename fixpoint<R, V1, V2>::yfunc_t fixpoint<R, V1, V2>::fix = [](tfunc_t f) -> func_t {
    return [f](fixpoint<R, V1, V2>::loopfunc_t x){  return f(x(x)); }
    ([f](fixpoint<R, V1, V2>::loopfunc_t x) -> fixpoint<R, V1, V2>::func_t{
        auto &ff = f;
        return [ff, x](t2t<decltype(x)>::t::Parameter1_t v1, 
            t2t<decltype(x)>::t::Parameter1_t v2){
            return ff(x(x))(v1, v2);
        }; 
    });
};

int _tmain(int argc, _TCHAR* argv[])
{
    auto term = [](int a)->int {
      return a*a;
    };

    auto next = [](int a)->int {
      return ++a;
    };

    auto sum = fixpoint<int, int, int>::fix(
    [term,next](std::function<int (int, int)> sum1) -> std::function<int (int, int)>{
        auto &term1 = term;
        auto &next1 = next;
        return [term1, next1, sum1](int a, int b)mutable ->int {
            if(a>b)
                return 0;
        else
            return term1(a) + sum1(next1(a),b);
        };
    });

    std::cout<<sum(1,10)<<std::endl; //385

    return 0;
}

Est-il possible de rendre cette réponse agnostique?
rayryeng

-2

Vous essayez de capturer une variable (somme) que vous êtes en train de définir. Ça ne peut pas être bon.

Je ne pense pas que des lambdas C ++ 0x véritablement auto-récursifs soient possibles. Cependant, vous devriez pouvoir capturer d'autres lambdas.


3
mais cela fonctionne si la déclaration de somme est changée de 'auto' à std :: function <int (int, int)> sans changer la liste de capture.
weima

Parce que ce n'est plus un lambda alors, mais une fonction qui peut être utilisée à la place de lambda?
Hamish Grubijan le

-2

Cette réponse est inférieure à celle des Yankes, mais quand même, la voici:

using dp_type = void (*)();

using fp_type = void (*)(dp_type, unsigned, unsigned);

fp_type fp = [](dp_type dp, unsigned const a, unsigned const b) {
  ::std::cout << a << ::std::endl;
  return reinterpret_cast<fp_type>(dp)(dp, b, a + b);
};

fp(reinterpret_cast<dp_type>(fp), 0, 1);

Je pense que vous devriez éviter reinterpret_cast. Le meilleur moyen dans votre cas est probablement de créer une structure qui remplace dp_type. Il doit avoir un champ fp_type, peut être construit à partir de fp_typeet avoir un opérateur ()avec des arguments comme fp_type. Ce sera proche de std::functionmais permettra un argument d'auto-référencement.
Yankes

Je voulais publier un exemple minimal, sans structure, n'hésitez pas à modifier ma réponse et à fournir une solution plus complète. A structajouterait également un niveau supplémentaire d'indirection. L'exemple fonctionne et le casting est conforme aux normes, je ne sais pas à quoi cela -1servait.
user1095108

non, struct fonctionnera uniquement comme conteneur pour le pointeur et sera passé en tant que valeur. Ce ne sera pas plus indirection ou surcharge que le pointeur. Et à propos de -1je ne savais pas qui vous l'a donné, mais je pense que c'est parce qu'il reinterpret_castdevrait être utilisé en dernier recours.
Yankes

Le castest censé fonctionner selon la norme c ++ 11. L'utilisation d'un struct, à mes yeux, pourrait vaincre l'utilisation d'un objet lambda. Après tout, le que structvous proposez est un foncteur, utilisant un objet lambda.
user1095108

Regardez la solution @Pseudonym, supprimez uniquement std::functionet vous aurez quelque chose de proche de celui que j'avais en tête. Cela aura probablement des performances similaires à votre solution.
Yankes

-3

Vous avez besoin d'un combinateur à virgule fixe. Regarde ça .

ou regardez le code suivant:

//As decltype(variable)::member_name is invalid currently, 
//the following template is a workaround.
//Usage: t2t<decltype(variable)>::t::member_name
template<typename T>
struct t2t
{
    typedef T t;
};

template<typename R, typename V>
struct fixpoint
{
    typedef std::function<R (V)> func_t;
    typedef std::function<func_t (func_t)> tfunc_t;
    typedef std::function<func_t (tfunc_t)> yfunc_t;

    class loopfunc_t {
    public:
        func_t operator()(loopfunc_t v)const {
            return func(v);
        }
        template<typename L>
        loopfunc_t(const L &l):func(l){}
        typedef V Parameter_t;
    private:
        std::function<func_t (loopfunc_t)> func;
    };
    static yfunc_t fix;
};
template<typename R, typename V>
typename fixpoint<R, V>::yfunc_t fixpoint<R, V>::fix = 
[](fixpoint<R, V>::tfunc_t f) -> fixpoint<R, V>::func_t {
    fixpoint<R, V>::loopfunc_t l = [f](fixpoint<R, V>::loopfunc_t x) ->
        fixpoint<R, V>::func_t{
            //f cannot be captured since it is not a local variable
            //of this scope. We need a new reference to it.
            auto &ff = f;
            //We need struct t2t because template parameter
            //V is not accessable in this level.
            return [ff, x](t2t<decltype(x)>::t::Parameter_t v){
                return ff(x(x))(v); 
            };
        }; 
        return l(l);
    };

int _tmain(int argc, _TCHAR* argv[])
{
    int v = 0;
    std::function<int (int)> fac = 
    fixpoint<int, int>::fix([](std::function<int (int)> f)
        -> std::function<int (int)>{
        return [f](int i) -> int{
            if(i==0) return 1;
            else return i * f(i-1);
        };
    });

    int i = fac(10);
    std::cout << i; //3628800
    return 0;
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.