Modèles C ++ Turing-complets?


Réponses:


110

Exemple

#include <iostream>

template <int N> struct Factorial
{
    enum { val = Factorial<N-1>::val * N };
};

template<>
struct Factorial<0>
{
    enum { val = 1 };
};

int main()
{
    // Note this value is generated at compile time.
    // Also note that most compilers have a limit on the depth of the recursion available.
    std::cout << Factorial<4>::val << "\n";
}

C'était un peu amusant mais pas très pratique.

Pour répondre à la deuxième partie de la question:
ce fait est-il utile en pratique?

Réponse courte: en quelque sorte.

Réponse longue: Oui, mais uniquement si vous êtes un démon de modèle.

Faire une bonne programmation en utilisant une métaprogrammation modèle qui est vraiment utile pour d'autres (c'est-à-dire une bibliothèque) est vraiment très difficile (bien que faisable). Pour aider à booster a même MPL aka (Meta Programming Library). Mais essayez de déboguer une erreur de compilateur dans votre code de modèle et vous serez dans une longue course difficile.

Mais un bon exemple pratique d'utilisation pour quelque chose d'utile:

Scott Meyers a travaillé sur des extensions du langage C ++ (j'utilise le terme vaguement) en utilisant les fonctionnalités de création de modèles. Vous pouvez en savoir plus sur son travail ici ' Application des fonctionnalités du code '


36
Dang y est allé concepts (pouf)
Martin York

5
Je n'ai qu'un petit problème avec l'exemple fourni - il n'exploite pas l'exhaustivité (complète) de Turing du système de modèles de C ++. Factorielle peut être trouvée également en utilisant des fonctions récursives primitives, qui ne sont pas complètes
Dalibor Frivaldsky

4
et maintenant nous avons des concepts lite
nurettin

1
En 2017, nous repoussons les concepts encore plus loin. Voici de l'espoir pour 2020.
DeiDei

2
@MarkKegel 12 ans plus tard: D
Victor le

181

J'ai fait une machine de turing en C ++ 11. Les fonctionnalités ajoutées par C ++ 11 ne sont en effet pas significatives pour la machine de turing. Il fournit simplement des listes de règles de longueur arbitraire utilisant des modèles variadiques, au lieu d'utiliser une métaprogrammation de macro perverse :). Les noms des conditions sont utilisés pour générer un diagramme sur stdout. J'ai supprimé ce code pour garder l'exemple court.

#include <iostream>

template<bool C, typename A, typename B>
struct Conditional {
    typedef A type;
};

template<typename A, typename B>
struct Conditional<false, A, B> {
    typedef B type;
};

template<typename...>
struct ParameterPack;

template<bool C, typename = void>
struct EnableIf { };

template<typename Type>
struct EnableIf<true, Type> {
    typedef Type type;
};

template<typename T>
struct Identity {
    typedef T type;
};

// define a type list 
template<typename...>
struct TypeList;

template<typename T, typename... TT>
struct TypeList<T, TT...>  {
    typedef T type;
    typedef TypeList<TT...> tail;
};

template<>
struct TypeList<> {

};

template<typename List>
struct GetSize;

template<typename... Items>
struct GetSize<TypeList<Items...>> {
    enum { value = sizeof...(Items) };
};

template<typename... T>
struct ConcatList;

template<typename... First, typename... Second, typename... Tail>
struct ConcatList<TypeList<First...>, TypeList<Second...>, Tail...> {
    typedef typename ConcatList<TypeList<First..., Second...>, 
                                Tail...>::type type;
};

template<typename T>
struct ConcatList<T> {
    typedef T type;
};

template<typename NewItem, typename List>
struct AppendItem;

template<typename NewItem, typename...Items>
struct AppendItem<NewItem, TypeList<Items...>> {
    typedef TypeList<Items..., NewItem> type;
};

template<typename NewItem, typename List>
struct PrependItem;

template<typename NewItem, typename...Items>
struct PrependItem<NewItem, TypeList<Items...>> {
    typedef TypeList<NewItem, Items...> type;
};

template<typename List, int N, typename = void>
struct GetItem {
    static_assert(N > 0, "index cannot be negative");
    static_assert(GetSize<List>::value > 0, "index too high");
    typedef typename GetItem<typename List::tail, N-1>::type type;
};

template<typename List>
struct GetItem<List, 0> {
    static_assert(GetSize<List>::value > 0, "index too high");
    typedef typename List::type type;
};

template<typename List, template<typename, typename...> class Matcher, typename... Keys>
struct FindItem {
    static_assert(GetSize<List>::value > 0, "Could not match any item.");
    typedef typename List::type current_type;
    typedef typename Conditional<Matcher<current_type, Keys...>::value, 
                                 Identity<current_type>, // found!
                                 FindItem<typename List::tail, Matcher, Keys...>>
        ::type::type type;
};

template<typename List, int I, typename NewItem>
struct ReplaceItem {
    static_assert(I > 0, "index cannot be negative");
    static_assert(GetSize<List>::value > 0, "index too high");
    typedef typename PrependItem<typename List::type, 
                             typename ReplaceItem<typename List::tail, I-1,
                                                  NewItem>::type>
        ::type type;
};

template<typename NewItem, typename Type, typename... T>
struct ReplaceItem<TypeList<Type, T...>, 0, NewItem> {
    typedef TypeList<NewItem, T...> type;
};

enum Direction {
    Left = -1,
    Right = 1
};

template<typename OldState, typename Input, typename NewState, 
         typename Output, Direction Move>
struct Rule {
    typedef OldState old_state;
    typedef Input input;
    typedef NewState new_state;
    typedef Output output;
    static Direction const direction = Move;
};

template<typename A, typename B>
struct IsSame {
    enum { value = false }; 
};

template<typename A>
struct IsSame<A, A> {
    enum { value = true };
};

template<typename Input, typename State, int Position>
struct Configuration {
    typedef Input input;
    typedef State state;
    enum { position = Position };
};

template<int A, int B>
struct Max {
    enum { value = A > B ? A : B };
};

template<int n>
struct State {
    enum { value = n };
    static char const * name;
};

template<int n>
char const* State<n>::name = "unnamed";

struct QAccept {
    enum { value = -1 };
    static char const* name;
};

struct QReject {
    enum { value = -2 };
    static char const* name; 
};

#define DEF_STATE(ID, NAME) \
    typedef State<ID> NAME ; \
    NAME :: name = #NAME ;

template<int n>
struct Input {
    enum { value = n };
    static char const * name;

    template<int... I>
    struct Generate {
        typedef TypeList<Input<I>...> type;
    };
};

template<int n>
char const* Input<n>::name = "unnamed";

typedef Input<-1> InputBlank;

#define DEF_INPUT(ID, NAME) \
    typedef Input<ID> NAME ; \
    NAME :: name = #NAME ;

template<typename Config, typename Transitions, typename = void> 
struct Controller {
    typedef Config config;
    enum { position = config::position };

    typedef typename Conditional<
        static_cast<int>(GetSize<typename config::input>::value) 
            <= static_cast<int>(position),
        AppendItem<InputBlank, typename config::input>,
        Identity<typename config::input>>::type::type input;
    typedef typename config::state state;

    typedef typename GetItem<input, position>::type cell;

    template<typename Item, typename State, typename Cell>
    struct Matcher {
        typedef typename Item::old_state checking_state;
        typedef typename Item::input checking_input;
        enum { value = IsSame<State, checking_state>::value && 
                       IsSame<Cell,  checking_input>::value
        };
    };
    typedef typename FindItem<Transitions, Matcher, state, cell>::type rule;

    typedef typename ReplaceItem<input, position, typename rule::output>::type new_input;
    typedef typename rule::new_state new_state;
    typedef Configuration<new_input, 
                          new_state, 
                          Max<position + rule::direction, 0>::value> new_config;

    typedef Controller<new_config, Transitions> next_step;
    typedef typename next_step::end_config end_config;
    typedef typename next_step::end_input end_input;
    typedef typename next_step::end_state end_state;
    enum { end_position = next_step::position };
};

template<typename Input, typename State, int Position, typename Transitions>
struct Controller<Configuration<Input, State, Position>, Transitions, 
                  typename EnableIf<IsSame<State, QAccept>::value || 
                                    IsSame<State, QReject>::value>::type> {
    typedef Configuration<Input, State, Position> config;
    enum { position = config::position };
    typedef typename Conditional<
        static_cast<int>(GetSize<typename config::input>::value) 
            <= static_cast<int>(position),
        AppendItem<InputBlank, typename config::input>,
        Identity<typename config::input>>::type::type input;
    typedef typename config::state state;

    typedef config end_config;
    typedef input end_input;
    typedef state end_state;
    enum { end_position = position };
};

template<typename Input, typename Transitions, typename StartState>
struct TuringMachine {
    typedef Input input;
    typedef Transitions transitions;
    typedef StartState start_state;

    typedef Controller<Configuration<Input, StartState, 0>, Transitions> controller;
    typedef typename controller::end_config end_config;
    typedef typename controller::end_input end_input;
    typedef typename controller::end_state end_state;
    enum { end_position = controller::end_position };
};

#include <ostream>

template<>
char const* Input<-1>::name = "_";

char const* QAccept::name = "qaccept";
char const* QReject::name = "qreject";

int main() {
    DEF_INPUT(1, x);
    DEF_INPUT(2, x_mark);
    DEF_INPUT(3, split);

    DEF_STATE(0, start);
    DEF_STATE(1, find_blank);
    DEF_STATE(2, go_back);

    /* syntax:  State, Input, NewState, Output, Move */
    typedef TypeList< 
        Rule<start, x, find_blank, x_mark, Right>,
        Rule<find_blank, x, find_blank, x, Right>,
        Rule<find_blank, split, find_blank, split, Right>,
        Rule<find_blank, InputBlank, go_back, x, Left>,
        Rule<go_back, x, go_back, x, Left>,
        Rule<go_back, split, go_back, split, Left>,
        Rule<go_back, x_mark, start, x, Right>,
        Rule<start, split, QAccept, split, Left>> rules;

    /* syntax: initial input, rules, start state */
    typedef TuringMachine<TypeList<x, x, x, x, split>, rules, start> double_it;
    static_assert(IsSame<double_it::end_input, 
                         TypeList<x, x, x, x, split, x, x, x, x>>::value, 
                "Hmm... This is borky!");
}

131
Vous avez beaucoup trop de temps libre.
Mark Kegel

2
Cela ressemble à lisp sauf avec un mot certin remplaçant toutes ces parenthèses.
Simon Kuang

1
La source complète est-elle accessible au public quelque part, pour le lecteur curieux? :)
OJFord

1
Juste la tentative mérite beaucoup plus de crédit :-) Ce code compile (gcc-4.9) mais ne donne aucun résultat - un peu plus d'informations, comme un article de blog, serait génial.
Alfred Bratterud

2
@OllieFord J'en ai trouvé une version sur une page pastebin et je l'ai repâtée ici: coliru.stacked-crooked.com/a/de06f2f63f905b7e .
Johannes Schaub - litb


13

Mon C ++ est un peu rouillé, donc peut-être pas parfait, mais c'est proche.

template <int N> struct Factorial
{
    enum { val = Factorial<N-1>::val * N };
};

template <> struct Factorial<0>
{
    enum { val = 1 };
}

const int num = Factorial<10>::val;    // num set to 10! at compile time.

Le but est de démontrer que le compilateur évalue complètement la définition récursive jusqu'à ce qu'il atteigne une réponse.


1
Euh ... n'avez-vous pas besoin d'avoir "template <>" sur la ligne avant struct Factorial <0> pour indiquer la spécialisation du template?
paxos1977

11

Pour donner un exemple non trivial: http://gitorious.org/metatrace , un traceur de rayons au moment de la compilation C ++.

Notez que C ++ 0x ajoutera une fonctionnalité non-modèle, au moment de la compilation et complète sous la forme de constexpr:

constexpr unsigned int fac (unsigned int u) {
        return (u<=1) ? (1) : (u*fac(u-1));
}

Vous pouvez utiliser constexpr-expression partout où vous avez besoin de constantes de temps de compilation, mais vous pouvez également appeler constexpr-functions avec des paramètres non const.

Une chose intéressante est que cela activera enfin les calculs en virgule flottante au moment de la compilation, bien que la norme stipule explicitement que les arithmétiques en virgule flottante au moment de la compilation n'ont pas à correspondre à l'arithmétique en virgule flottante à l'exécution:

bool f(){
    char array[1+int(1+0.2-0.1-0.1)]; //Must be evaluated during translation
    int  size=1+int(1+0.2-0.1-0.1); //May be evaluated at runtime
    return sizeof(array)==size;
}

Il n'est pas spécifié si la valeur de f () sera vraie ou fausse.



8

L'exemple factoriel ne montre pas en fait que les modèles sont complets de Turing, autant qu'il montre qu'ils prennent en charge la récursivité primitive. Le moyen le plus simple de montrer que les modèles sont complets est la thèse de Church-Turing, c'est-à-dire en implémentant soit une machine de Turing (désordonnée et un peu inutile), soit les trois règles (app, abs var) du calcul lambda non typé. Ce dernier est beaucoup plus simple et beaucoup plus intéressant.

Ce qui est discuté est une fonctionnalité extrêmement utile quand on comprend que les modèles C ++ permettent une programmation fonctionnelle pure à la compilation, un formalisme expressif, puissant et élégant mais aussi très compliqué à écrire si vous avez peu d'expérience. Notez également combien de personnes trouvent que le simple fait d'obtenir du code fortement modélisé peut souvent demander un gros effort: c'est exactement le cas des langages fonctionnels (purs), qui rendent la compilation plus difficile mais produisent étonnamment du code qui ne nécessite pas de débogage.


Hé, quelles sont les trois règles auxquelles vous faites référence, je me demande, par "app, abs, var"? Je suppose que les deux premiers sont respectivement l'application de la fonction et l'abstraction (définition lambda (?)). Est-ce vrai? Et quel est le troisième? Quelque chose à voir avec les variables?
Wizek le

Je pense personnellement qu'il serait généralement préférable qu'un langage prenne en charge la récursivité primitive dans le compilateur plutôt que Turing Complete, car un compilateur pour un langage prenant en charge la récursivité primitive au moment de la compilation pourrait garantir que toute construction se terminera ou échouera, mais celui dont le processus de construction est Turing Complete ne le peut pas, sauf en contraignant artificiellement la construction afin qu'elle ne soit pas Turing Complete.
supercat

5

Je pense que cela s'appelle la méta-programmation de modèle .


2
C'est le côté utile de celui-ci. L'inconvénient est que je doute que la plupart des gens (et certainement pas moi) comprendront jamais même un petit pourcentage de ce qui se passe dans la plupart de ces choses. C'est horriblement illisible, impossible à maintenir.
Michael Burr

3
C'est l'inconvénient de tout le langage C ++, je pense. Ça devient un monstre ...
Federico A. Ramponi

C ++ 0x promet de le rendre beaucoup plus facile (et d'après mon expérience, le plus gros problème est les compilateurs qui ne le prennent pas entièrement en charge, ce que C ++ 0x n'aidera pas). Les concepts en particulier semblent éclaircir les choses, comme se débarrasser de beaucoup de choses SFINAE, ce qui est difficile à lire.
coppro

@MichaelBurr Le comité C ++ ne se soucie pas des éléments illisibles, non maintenables; ils adorent ajouter des fonctionnalités.
Sapphire_Brick

4

Eh bien, voici une implémentation de Turing Machine au moment de la compilation exécutant un castor occupé à 4 états à 2 symboles

#include <iostream>

#pragma mark - Tape

constexpr int Blank = -1;

template<int... xs>
class Tape {
public:
    using type = Tape<xs...>;
    constexpr static int length = sizeof...(xs);
};

#pragma mark - Print

template<class T>
void print(T);

template<>
void print(Tape<>) {
    std::cout << std::endl;
}

template<int x, int... xs>
void print(Tape<x, xs...>) {
    if (x == Blank) {
        std::cout << "_ ";
    } else {
        std::cout << x << " ";
    }
    print(Tape<xs...>());
}

#pragma mark - Concatenate

template<class, class>
class Concatenate;

template<int... xs, int... ys>
class Concatenate<Tape<xs...>, Tape<ys...>> {
public:
    using type = Tape<xs..., ys...>;
};

#pragma mark - Invert

template<class>
class Invert;

template<>
class Invert<Tape<>> {
public:
    using type = Tape<>;
};

template<int x, int... xs>
class Invert<Tape<x, xs...>> {
public:
    using type = typename Concatenate<
        typename Invert<Tape<xs...>>::type,
        Tape<x>
    >::type;
};

#pragma mark - Read

template<int, class>
class Read;

template<int n, int x, int... xs>
class Read<n, Tape<x, xs...>> {
public:
    using type = typename std::conditional<
        (n == 0),
        std::integral_constant<int, x>,
        Read<n - 1, Tape<xs...>>
    >::type::type;
};

#pragma mark - N first and N last

template<int, class>
class NLast;

template<int n, int x, int... xs>
class NLast<n, Tape<x, xs...>> {
public:
    using type = typename std::conditional<
        (n == sizeof...(xs)),
        Tape<xs...>,
        NLast<n, Tape<xs...>>
    >::type::type;
};

template<int, class>
class NFirst;

template<int n, int... xs>
class NFirst<n, Tape<xs...>> {
public:
    using type = typename Invert<
        typename NLast<
            n, typename Invert<Tape<xs...>>::type
        >::type
    >::type;
};

#pragma mark - Write

template<int, int, class>
class Write;

template<int pos, int x, int... xs>
class Write<pos, x, Tape<xs...>> {
public:
    using type = typename Concatenate<
        typename Concatenate<
            typename NFirst<pos, Tape<xs...>>::type,
            Tape<x>
        >::type,
        typename NLast<(sizeof...(xs) - pos - 1), Tape<xs...>>::type
    >::type;
};

#pragma mark - Move

template<int, class>
class Hold;

template<int pos, int... xs>
class Hold<pos, Tape<xs...>> {
public:
    constexpr static int position = pos;
    using tape = Tape<xs...>;
};

template<int, class>
class Left;

template<int pos, int... xs>
class Left<pos, Tape<xs...>> {
public:
    constexpr static int position = typename std::conditional<
        (pos > 0),
        std::integral_constant<int, pos - 1>,
        std::integral_constant<int, 0>
    >::type();

    using tape = typename std::conditional<
        (pos > 0),
        Tape<xs...>,
        Tape<Blank, xs...>
    >::type;
};

template<int, class>
class Right;

template<int pos, int... xs>
class Right<pos, Tape<xs...>> {
public:
    constexpr static int position = pos + 1;

    using tape = typename std::conditional<
        (pos < sizeof...(xs) - 1),
        Tape<xs...>,
        Tape<xs..., Blank>
    >::type;
};

#pragma mark - States

template <int>
class Stop {
public:
    constexpr static int write = -1;
    template<int pos, class tape> using move = Hold<pos, tape>;
    template<int x> using next = Stop<x>;
};

#define ADD_STATE(_state_)      \
template<int>                   \
class _state_ { };

#define ADD_RULE(_state_, _read_, _write_, _move_, _next_)          \
template<>                                                          \
class _state_<_read_> {                                             \
public:                                                             \
    constexpr static int write = _write_;                           \
    template<int pos, class tape> using move = _move_<pos, tape>;   \
    template<int x> using next = _next_<x>;                         \
};

#pragma mark - Machine

template<template<int> class, int, class>
class Machine;

template<template<int> class State, int pos, int... xs>
class Machine<State, pos, Tape<xs...>> {
    constexpr static int symbol = typename Read<pos, Tape<xs...>>::type();
    using state = State<symbol>;

    template<int x>
    using nextState = typename State<symbol>::template next<x>;

    using modifiedTape = typename Write<pos, state::write, Tape<xs...>>::type;
    using move = typename state::template move<pos, modifiedTape>;

    constexpr static int nextPos = move::position;
    using nextTape = typename move::tape;

public:
    using step = Machine<nextState, nextPos, nextTape>;
};

#pragma mark - Run

template<class>
class Run;

template<template<int> class State, int pos, int... xs>
class Run<Machine<State, pos, Tape<xs...>>> {
    using step = typename Machine<State, pos, Tape<xs...>>::step;

public:
    using type = typename std::conditional<
        std::is_same<State<0>, Stop<0>>::value,
        Tape<xs...>,
        Run<step>
    >::type::type;
};

ADD_STATE(A);
ADD_STATE(B);
ADD_STATE(C);
ADD_STATE(D);

ADD_RULE(A, Blank, 1, Right, B);
ADD_RULE(A, 1, 1, Left, B);

ADD_RULE(B, Blank, 1, Left, A);
ADD_RULE(B, 1, Blank, Left, C);

ADD_RULE(C, Blank, 1, Right, Stop);
ADD_RULE(C, 1, 1, Left, D);

ADD_RULE(D, Blank, 1, Right, D);
ADD_RULE(D, 1, Blank, Right, A);

using tape = Tape<Blank>;
using machine = Machine<A, 0, tape>;
using result = Run<machine>::type;

int main() {
    print(result());
    return 0;
}

Preuve Ideone: https://ideone.com/MvBU3Z

Explication: http://victorkomarov.blogspot.ru/2016/03/compile-time-turing-machine.html

Github avec plus d'exemples: https://github.com/fnz/CTTM


3

Vous pouvez consulter cet article du Dr Dobbs sur une implémentation FFT avec des modèles que je ne trouve pas si triviaux. Le point principal est de permettre au compilateur d'effectuer une meilleure optimisation que pour les implémentations sans modèle car l'algorithme FFT utilise beaucoup de constantes (tables sin par exemple)

partie I

deuxieme PARTIE


2

Il est également amusant de souligner qu'il s'agit d'un langage purement fonctionnel, bien qu'il soit presque impossible à déboguer. Si vous regardez le message de James, vous verrez ce que je veux dire par être fonctionnel. En général, ce n'est pas la fonctionnalité la plus utile de C ++. Il n'a pas été conçu pour cela. C'est quelque chose qui a été découvert.


2

Cela peut être utile si vous souhaitez calculer des constantes au moment de la compilation, du moins en théorie. Découvrez la métaprogrammation des modèles .


1

Un exemple qui est raisonnablement utile est une classe de ratio. Il existe quelques variantes qui flottent. Attraper le cas D == 0 est assez simple avec des surcharges partielles. Le véritable calcul consiste à calculer le GCD de N et D et à compiler le temps. Ceci est essentiel lorsque vous utilisez ces ratios dans les calculs de compilation.

Exemple: lorsque vous calculez centimètres (5) * kilomètres (5), au moment de la compilation, vous multipliez le rapport <1,100> et le rapport <1000,1>. Pour éviter tout débordement, vous voulez un ratio <10,1> au lieu d'un ratio <1000,100>.


0

Une machine de Turing est Turing-complète, mais cela ne signifie pas que vous devriez en utiliser une pour le code de production.

Essayer de faire quelque chose de non trivial avec des modèles est selon mon expérience désordonné, laid et inutile. Vous n'avez aucun moyen de "déboguer" votre "code", les messages d'erreur lors de la compilation seront cryptiques et généralement dans les endroits les plus improbables, et vous pouvez obtenir les mêmes avantages de performances de différentes manières. (Indice: 4! = 24). Pire encore, votre code est incompréhensible pour le programmeur C ++ moyen, et ne sera probablement pas portable en raison du large éventail de niveaux de support dans les compilateurs actuels.

Les modèles sont parfaits pour la génération de code générique (classes de conteneurs, wrappers de classe, mix-ins), mais non - à mon avis, l'exhaustivité des modèles de Turing n'est PAS UTILE en pratique.


4! peut-être 24 ans, mais qu'est-ce que MY_FAVORITE_MACRO_VALUE! ? OK, je ne pense pas non plus que ce soit une bonne idée.
Jeffrey L Whitledge

0

Juste un autre exemple de comment ne pas programmer:

template <int Depth, int A, typename B>
struct K17 {
    statique const int x =
    K17 <Profondeur + 1, 0, K17 <Profondeur, A, B>> :: x
    + K17 <Profondeur + 1, 1, K17 <Profondeur, A, B>> :: x
    + K17 <Profondeur + 1, 2, K17 <Profondeur, A, B>> :: x
    + K17 <Profondeur + 1, 3, K17 <Profondeur, A, B>> :: x
    + K17 <Profondeur + 1, 4, K17 <Profondeur, A, B>> :: x;
};
template <int A, nom de type B>
struct K17 <16, A, B> {statique const int x = 1; };
statique const int z = K17 <0,0, int> :: x;
void main (void) {}

Les modèles de publication sur C ++ sont terminés


pour les curieux, la réponse pour x est pow (profondeur 5,17);
volé le

Ce qui est beaucoup plus simple à voir lorsque vous réalisez que les arguments de modèle A et B ne font rien et les suppriment, puis remplacent tous les ajouts par K17<Depth+1>::x * 5.
David Stone
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.