La programmation en C++ moderne

Apprenez la programmation de zéro jusqu'à l'infini !

a marqué ce sujet comme résolu.

Bonjour les agrumes !

La bêta a été mise à jour et décante sa pulpe à l’adresse suivante :

Merci d’avance pour vos commentaires.


Salut à tous.

Une petite mise à jour pour rajouter une partie dédiée au constructeur par défaut et à l’initialisation directe des attributs. Merci d’avance pour vos retours.

Ce n’est pas pratique de ne pas avoir de diff, pour suivre les corrections sans devoir tout relire. Pour ça que j’aime bien rédiger sur GitHub puis copier-coller sur ZdS.

Remarque générale sur le cours et la pédagogie (en prenant le chapitre "Une classe de grande valeur" comme exemple). Une question qui revient souvent chez les débutants, c’est : "parmis toutes les syntaxes que j’ai appris, laquelle choisir ?" Cela correspond à la question du pourquoi des syntaxes.

En termes pédagogique, cela veut dire de penser les chapitres par rapport aux problématiques que l’on veut présenter, pas les syntaxes.

Ce chapitre présente le même problème : "pourquoi la sémantique de valeur ?"


Copier des objets

Peut être commencer par faire un rappel sur les copies qu’ils ont déjà vu et (normalement) manipulés. Par exemple :

  1. Montrer que les exemples suivants sont comparable avec == donc ce sont des sémantiques de valeurs

  2. copie de type fondamentaux (qui sont des semantiques de valeurs, mais ne sont pas des classes)

int j { i };
j == i; 
  1. structure user-def, on peut copier parce que le compilateur ajoute automatiquement ce qu’il faut.
struct A { int i; };
A a;
A b { a };
  1. exemple de classe connues et copiables. Autres exemple de classe de valeur : std::vector, iterqteurs, std::pair, etc.
std::string s2 { s1 };

il faut alors définir un constructeur de copie.

Cf juste avant, ils ont normalement déjà vu des cas où il est possible de copier, mais sans avoir défini de contructeur par copie. Il faut en parler rapidement (au moins pour dire que ca sera détaillé plus tard).

Reprenons notre classe Fraction (j’ai simplifié une partie du code pour plus de facilité et de lisibilité) et implémentons le constructeur de copie.

En terme de progression pédagogique, les lecteurs voient :

  • la copie de structure de données sans avoir besoin de déclarer quoi que ce soit pour faire une copie (dans les chapitres précédants, sans rappel dans ce chapitre)
  • la syntaxe pour déclarer et définir la copie
  • la sytnaxe pour déclarer la copie et laisser le compilateur s’occuper d’écrire la copie

Donc pas forcément une progression logique en termes de complexité du code et des notions. A mon sens, il serait plus progressif de faire :

  • rappel sur la copie implicite (faites automatiquement par le compilateur)
  • présenter la déclaration de la copie (définition implicite).
  • puis la syntaxe la plus complexe : la déclaration et définition implicite
    int m_numerateur;
    int m_denominateur;

Je ne suis pas un grand fan des attributs non initialisés.

Constructeur de copie Opérateur d’affectation par copie

"par" ou "de" ?

Fraction(int n, int d);

Normalement, ils ont vu les paramètres par défaut dans le chapitre sur les fonctions (?). Ne faudrait-il pas mettre une note pour dire que c’est utilisable aussi avec les constructeurs ? (Exemple avec Fraction(int n, int d = 1);)

Et du coup, parler aussi du mot clé explicit. (Et du coup, le titre "Soyons explicites" peut etre confondu avec ce mot clé)

Le compilateur, qu’il est choux !

Du coup, vous présentez une syntaxe, puis juste après, vous dites que ça sert à rien…

Le lecteur peut légitiment se poser la question "pourquoi ?" (pourquoi avoir besoin de connaitre cette syntaxe ? Quand avoir besoin de l’écrire explicitement ?)

Si on suit les bonnes pratiques (Big-0), on ne devrait pas avoir à écrire explicitement la copie. (Est-ce que vous parlez de Big-0 quelque part ?). Donc en gros, on explique une syntaxe qu’ils ne devront pas utiliser s’ils suivent les bonnes pratiques…

Cela rejoint ce que je disais au début : présenter selon les problématiques qu’on veut résoudre. Ici, on présente la copie (écrire la copie, pas l’utiliser) parce qu’elle répond a une problématique ou juste parce qu’on a toujours organisé les cours POO de C++ comme ça ?

(J’utilise "on" parce que je présentais la copie aussi dans le chapitre 2 de la POO. Mais c’est peut être pas une bonne idée)

On dit d’une classe qu’elle a une sémantique de valeur si deux objets au contenu identique peuvent être considérés comme égaux.

(Retour au début du chapitre). On définie la sémantique de valeur par la notion d’égalité, mais le chapitre ne parle pas de l’égalité, il parle de la copie (qui est une notion discutable à présenter). Il faudrait présenter en premier la notion d’égalité (et Shika me fait remarquer en live que si on a des notions de théorie des langages, cette notion d’égalité n’est pas trivial, il existe plusieurs concepts d’égalité).

La déf en C++ : https://en.cppreference.com/w/cpp/named_req/EqualityComparable donc :

  • a et b meme type T
  • a == b = true -> égalité

Je me demande s’il faut tout simplement déplacer la copie en annexe et parler directement de la surcharge d’opérateurs dans la sémantiques de valeur (en commançant par ==).

(Note : je viens de revoir le chapitre 6, la notion de surcharge d’opérateur à déjà été vu. Donc aucun problème à aborder cela dans ce chapitre)

Les opérateurs d’affectation intégrés

C’est pas l’écriture idiomatique (https://en.cppreference.com/w/cpp/language/operators) qui est utilisée. Pas sur que ce soit une bonne idée par rapport aux bonnes pratiques.

Fraction& Fraction::operator+=(Fraction const & autre_fraction) Fraction operator+(Fraction a, Fraction const & b)

Peut être une note par rapport à pourquoi et quand avoir des fonctions libres et des fonctions membres ?

Avez-vous remarqué que le code pour écrire += est quasiment identique à celui utilisé pour + ?

Le lecteur n’aura probablement pas remarqué cela, s’il a lu le chapitre 6 il y a plusieurs semaines.

Soyons intelligents, réutilisons Fraction f3 { f1 + f2 };

Peut être const.

En toute amitié

La notion d’amitié n’est pas liée aux opérateurs. Il ne faudrait pas présenter cette notion en même temps que private et public ? (En faisant attention a l’encapsulation)

d’une classe qu’elle est amie

L’amité classe-classe est citée mais pas expliquée ou présentée en pratique.

Or, si l’on veut l’implémenter comme membre de notre classe Fraction, on a alors le premier paramètre qui doit être de même type que la classe, c’est-à-dire Fraction.

La distinction entre fonction libre et membre n’est peut pas tres bien définie, ainsi que les sybtaxes correspondantes.

Vous pouvez vous servir du code suivant comme base, pour vous donnez une idée de comment la classe est utilisée.

Ca serait peut etre mieux de :

  • soit de demander dans un premier temps de définir eux même l’API et les tests unitaires
  • soit donner les tests unitaires, puis leur demander de définir l’API puis l’implémentation

n’êtes pas très à l’aide en mathématiques

"très à l’aise"

(Et ça manque de const)

Certaines classes ont une sémantique de valeur.

Pas fan de la formulation. Une classe n’a pas une sémantique, on choisit de donner une sémantique à une classe. Pour certaines raisons. Mais comme la question du pourquoi n’est pas abordée, le lecteur ne connait pas ces raisons (cf ma première remarque).

opérateur d’affection par recopie

"par recopie" ou "par copie" ? (Ou "de copie")


Petite note sur le chapitre suivant (je ferais la relecture complète au prochain live).

Pour être sûrs d’avoir bien codé, il est important de tester son code, vous le savez déjà. Alors, pourquoi en serait-il différent pour ce T.P ?

Du coup, si c’est important, pourquoi ce n’est pas dans la correction ?

Ce qui me gène dans la présentation des solutions d’exos et TP, ce qu’on laisse penser aux lecteurs que résoudre une problème de programmation, c’est pondre un code. Or, ça devrait être une démarche avant tout, de résolution de problème : comment aborder un problème, quels sont les étapes de sa résolution, etc. Cela devrait appraitre dans les solutions données (principe de donner le bon exemple, pour que l’apprenant suive cet exemple).

EDIT : j’ai fait la review en live, j’ai donné plus de détails a l’oral.

+2 -0

Merci pour tes retours. Effectivement , après analyse er relecture du chapitre, je me rends compte que j’ai oublié complètement de me mettre à la place d’un débutant. Et les notions présentées ne sont donc pas forcément claires ou évidentees.

Je vais me poser calmement pour faire un plan avant de repartir sur l’écriture. :)

PS : Tu peux relire le TP, mais n’y perd pas trop de temps, vu que je n’ai rien réécrit, excepté le code. Je compte le reprendre pour le détailler, lui donner une progression, des tests, etc.

Petite review rapide pour finir le TP. Je continerais probablement la review dans les prochains lives sur les premiers chapitres du cours.

[T.P] Entrons dans la matrice !

Pourquoi le chapitre sur le déplacement est après la sémantique d’entité ? A priori, c’est un concept qui peut être abordé avec la sémantique d’entité.

// Version d'accès en lecture, marche avec une matrice const.
int const & operator()(std::size_t x, std::size_t y) const;
// Version d'accès en écriture.
int& operator()(std::size_t x, std::size_t y);

Est-ce que cela a été expliqué pourquoi il faut généralement écrire les 2 versions des fonctions ?

Est-ce le exception et le thread safty a été abordé par rapport aux fonctions const ?

  • Constructeurs

Est-ce que le constructeur avec l’initialisateur_list devrait être aborder ici, avec la sémantique de valeur, ou plus tard.

Pour être sûrs d’avoir bien codé, il est important de tester son code, vous le savez déjà. Alors, pourquoi en serait-il différent pour ce T.P ?

Si c’est important, pour ne pas montrer le bon exemple dans le TP ? Et pourquoi parler des tests à la fin ? (TDD)

+0 -0

Salut à tous.

Je réfléchis depuis plusieurs jours à vos différentes remarques et je pense être arrivé à quelque chose, même si ça va demander un effort de réécriture. Y’a plein de notions qui peuvent potentiellement avoir leur place dès le premier chapitre, mais ça va le rendre indigeste. J’aimerais donc le scinder.

  1. Introduction à la POO, on repart sur les structures et on montre qu’on peut leur ajouter des fonctions membres. On joue avec ça, cool. Puis on oublie pas const. On parle ensuite des invariants qu’on aimerait que l’objet respecte. Sauf que tout public. Donc on introduit les histoires de visibilités. Mais du coup, comment initialiser une instance ? On a bien l’initialisation des attributs, mais c’est tout. Teaser pour le deuxième chapitre.
  2. On aborde les constructeurs. Pourquoi, comment ? On parle du constructeur par défaut, de delete et default, puis des constructeurs personnalisés, etc. On en profite aussi pour parler des conversions, donc de explicit, etc.
  3. On en arrive à la sémantique de valeur. On reparle donc à fond de la surcharge d’opérateurs, de l’égalité, versions libres / membres, les bonnes façons de faire, etc. On en profite pour introduire brièvement la copie, sans rentrer dans les détails, juste default.
  4. On a le TP sur les matrices, qui permet de pratiquer tout ce qu’on a vu, avec définitions des tests, de l’API.

J’avoue qu’avec ce plan je ne sais pas où caser l’amitié. Est-ce qu’elle répond vraiment à un besoin à ce stade ? Je ne sais pas trop. Je n’ai pas encore réfléchi à la suite du plan, mais j’aimerais déjà avoir vos retours sur ce que je propose.

Merci d’avance à tous.

Je réfléchi à cette histoire de structure qui s’objectise… Jusqu’à présent, c’était une approche que je rangeais dans "historique", car c’est comme ça que se sont constitutés les cours sauce 90, avec des fonctions init, et cette pensée omniprésente qu’un objet est un aggrégat de données avec des fonctions en plus. Quand je commence mon introduction, je tends à parler de FILE dont on ne connait pas le contenu. Et je passe à la version C++ de l’abstraction où cette fois le contenu est connu et où on utilise des fonctions membres. Bref, je ne pars pas de l’aggrégat syntaxique, mais d’un pseudo type abstrait de données: ça rend un service et on n’a que faire de comment s’est foutu, et voici la syntaxe.

Pour l’amitié, il y a 2 axes. Le premier est le plus connu: l’accès privilégié. Typiquement, on peut en parler à deux moments: la surcharge de certains opérateurs comme << et >>, et fonction libre VS fonction membre. Le second axe, pour lequel on commence à avoir des papiers et une conf, c’est pour éviter de polluer les recherches de résolution de nom en particulier avec des opérateurs. Ce qui simplifie les messages d’erreurs. Ce second axe participe à l’écriture canonique de opérateurs.

Je te rassure, je ne compte pas faire une structure à laquelle on rajoute une fonction init. Mon approche reste la même qu’actuellement, simplement que le premier chapitre se termine avec le fait qu’on aimerait bien une façon d’intialiser notre objet et de définir nos invariants, ce qui est un teaser pour le deuxième, qui parlera enfin des constructeurs.

Dans mon message, les fonctions init ne sont qu’un aspect mineur de ce qui me fait tiquer. L’aspect majeur est que l’on part des données et que l’on rajoute des fonctions. D’un point de vue philo, cela éloigne de la pensée services.

Quitte à y aller de manière incrémentale, je tendrai à définir un type à la FILE, mais manipulé via des références plutôt que des pointeurs. Mais cela alourdirai pour ajouter une étape inutile in fine.

struct stack;

void push(stack & s, int e);
void pop(stack & s);
size_t size(stack const& s);
int top(stack const& s);
stack new_stack();

Là, on montre que l’on s’intéresse avant tout au service et nullement à comment c’est foutu (array, vector, list…).

Et après, "En fait le langage offre nativement une syntaxe pour faire ça et ça se manipule avec s.func() au lieu de func(s).".

Je ne suis pas non plus un grand fan de cette approche de "une structure qui devient un objet". De mémoire, dans mon cours, je présenté les struct comme aggrégat de données, un peu comme des std::tuple mais avec des paramètres nommés. Donc juste une question d’expressivité.

using Date = std::tuple<int,int,int>;
vs
struct Date { int année, mois, jour; };

Mais pour la partie POO, je me focalisais sur la sémantique. (ie quel sens, quelle signification a une classe). En soi, expliquer ce qu’est une sémantique et pourquoi c’est intéressant en programmation n’est pas (plus) compliqué comme approche. Par exemple avec des unités de mesure.

Problème : valeur sans signification = on peut faire n’importe quoi avec.

int temperature = 37;
int poids = 70;
int calcul_idiot = temperature + poids; // pas d'erreur de compilation

Solution : donner une sémantique = définir ce qui est autorisé ou non de faire.

Degres temperature = 37;
Kilos poids = 70;
auto calcul_idiot = temperature + poids; // erreur de compilation

En donnant une sémantique :

  • on simplifie la compréhension du code (architecture et design du code)
  • on diminue le risque de bugs (avoir une erreur de compilation quand on écrit du code incorrect)

Et en parlant de ce qu’on a le droit de faire ou pas, on en arrive a discuter sur ce qu’est la validité des objets, puis l’initialisation et les invariants.

on aimerait bien une façon d’intialiser notre objet et de définir nos invariants

A mon avis, le problème est que c’est une question orientée sur la syntaxe (comment initialiser, comment définir les invariants) et pas sur la problématique (pourquoi initialiser et définit les invariants ? Quelle problématique essait-on de résoudre en introduisant ces syntaxes ?)

J’ai pas mal parlé de ca dans mon dernier live.

Je ne suis pas fan des structures opaques, ca me fait justement trop penser a du C. Mais a la rigueur, pourquoi pas.

+2 -0

C’est dur à expliquer sur téléphone et je pense que ça sera plus clair une fois la possibilité de relire, mais ce que tu proposes @gbdivers, c’est ce que je compte faire. Je veux dire par là que je ne compte pas centrer ça sur les données mais bien sur les services qu’un objet peut rendre.

Sauf si après vous me dites que cette approche n’est pas bonne, auquel cas je partirai sur une autre solution. Mais là, dites vous que le premier chapitre va simplement être scindé en deux et affiné.

Lynix a prevu de faire un cours C++ en live sur Twitch. Tu avais parlé de faire un live pour parler du cours C++. Du coup, ca serait peut etre bien de faire un live a plusieurs pour discuter pédagogie du C++.

Tu es sur le discord ? Pour organiser une date. A priori, ca sera un samedi ou dimanche (fin d’apres midi chez vous, matin chez moi)

+0 -0

Lynix a prevu de faire un cours C++ en live sur Twitch. Tu avais parlé de faire un live pour parler du cours C++. Du coup, ca serait peut etre bien de faire un live a plusieurs pour discuter pédagogie du C++.

Tu es sur le discord ? Pour organiser une date. A priori, ca sera un samedi ou dimanche (fin d’apres midi chez vous, matin chez moi)

gbdivers

Ça serait effectivement une bonne idée. Je ne serai pas là les deux prochaines semaines, donc si vous êtes d’accord pour plus tard, ça me va. Par contre, je n’ai pas de caméra, je sais pas si ça a une quelconque importance ou pas.

Salut à tous.

Une assez grosse mise à jour du chapitre 1 de la partie POO. Je l’ai réécris presque en totalité, pour bien montrer la démarche de réflexion sur l’interface, d’écriture des tests, sur le fait qu’on veut offrir des services et non plus manipuler des données, qui ne sont plus le point central.

Le chapitre suivant est totalement en mode brouillon, mais dedans on y abordera les constructeurs, dont celui par défaut, l’initialisation par défaut, que faire en cas d’erreur, etc.


Bonjour les agrumes !

La bêta a été mise à jour et décante sa pulpe à l’adresse suivante :

Merci d’avance pour vos commentaires.

Bonjour et félicitation pour le cours sur la POO.

Il y a longtemps que je n’avais pas relu … et je constate son évolution …

Pour l’introduction à la POO :

Il y a longtemps, quand je faisais mes études (35 ans, voir plus), la justification qu’on m’avait donnée à l’époque (par un prof qui découvrait certainement en même temps que nous), était la stabilité des interfaces ! Dans un sources, en cas de correction/évolution, ce qui varie le moins, c’est les interfaces (des fonctions membre). Aussi, par rapport à la programmation procédurale où le bug est potentiellement partout, on réduit les modifications/correction à l’intérieur de l’objet, mais vis a vis des utilisateurs de l’objet, il n’y a pas de besoin de modification. Bien sûr, après quelque temps sur les forums de C++, avec avec votre cours et les réponses de nos experts, j’ai compris qu’il y avait d’autre intèré, Mais ce point, je ne le retrouve pas dans votre présentation. Je pense que ça pourrait être une introduction au sous chapitre « Penser tests ». En effet pourquoi faire des TestsU, c’est pour valider les fonctions au plutôt, et ne plus revenir dessus, car les interfaces des fonctions membres sont figées … et comme on ne modifiera plus ces interfaces, on continu à bâtir sur ces objets, fiable, stable !

Définition des tests :

Pour moi, il ne suffit pas de dire « il faut réfléchir aux tests que nous allons écrire pour vérifier que notre implémentation est correcte. ». Dans mon domaine, il est demandé pour les tests U, de définir des tests fonctionnels (ou dans les « ranges des données ») des tests aux limites (en identifiant les valeurs limites, les points d’inflexions, …) de les tests de robustesse (valeurs hors limites). Je suis bien conscient, que c’est pas dans ce premier chapitre qu’il faut « matraquer » notre pôve étudiant en C++, mais comme j’ai compris que dans les chapitres qui suivent vous allez reparler des tests, ce serait peut-être bien d’ajouter un petit truc sur le methodo des tests.

Si je me permets ce matin de vous donné mon avis (alors que vu mon expérience en C++ je ne suis peut-être pas le plus légitime), c’est que j’ai reçu un MP me reconnaissant comme relecteur de ce cours. J’en ai été surpris, et je remercie celui qui à eu cette idée.

Bien cordialement.

+2 -0

Hello,

C’est intéressant ce que tu dis @Dedeun. Cela rejoint ce que je présente dans mes sessions lives. J’y remplace le trio historique (possiblement postérieur à tes études) encapsulation/héritage/polymorphisme par le quatuor Abstraction/Encapsulation/Import de Code/Sous-typage.

La stabilité des interfaces que tu présentes, c’est une conséquence de l’abstraction (qui n’a pas besoin du C++ pour exister, cf les Types Abstraits de Données, FILE*, etc). Je le distingue de l’encapsulation qui vise à protéger les invariants. Même si in-fine il n’y a qu’un seul mécanisme en C++ pour mettre en place ces deux.

Pour les tests de robustesse, cela m’évoque cette tendance où des gens écrivent des tests (U) pour voir comment des préconditions sont traitées. Cela me parait juste aberrant que de regarder comment réagit un programme à sqrt(-1), asin(2), memcpy(nullptr, nullptr, 1)… Cette pratique est la raison principale pour laquelle les fonctions avec narrow contract ne sont pas noexcept en C++.

Merci Lmghs pour cette réponse.

Pour les tests de robustesse, cela m’évoque cette tendance où des gens écrivent des tests (U) pour voir comment des préconditions sont traitées. Cela me parait juste aberrant que de regarder comment réagit un programme à sqrt(-1), asin(2), memcpy(nullptr, nullptr, 1).

lmghs

Non, je ne parle pas de tester la lib Standard, ou le code des autres, mais de tester son code. Déjà, parfois on n’a pas le choix (Assurance qualité, logiciel qualifié ou certifié, haut niveau de disponibilité, …), et puis ça peut faire partie aussi pour se convaincre soit même, que l’on à bien pris en compte les cas hors norme dans son code et que ça plantera pas.

Merci Ksass’peuk pour cette autre réponse.

Les tests hors limites ne devraient pas avoir lieu au niveau des interfaces. Ils n’ont même pas vraiment de sens. Précisément parce que si ton interface définit ce qui se passe hors limites alors … elle n’a pas de limites.

Ksass`Peuk

Bonne remarque Ksass’peuk. Tout vient de ce que l’on appel "Robustesse", et là c’est un problème de syntaxe. Dans mon monde (au boulot), "robustesse" veut dire "C’est pas autorisé, mais si c’est utilisé (par erreur, ou par malicieux), je maîtrise ce qui se passe".

Comme j’ouvre un nouveau post, j’en profite pour faire une nouvelle remarque (que j’avais oublié de faire ci dessus). M’sieur Informaticienzero, tu as fait tes tests en utilisant "assert" pour contrôler les résultats. Souvent il est utilisé des "cout «" dans les codes que l’on trouve. Et finalement, on peut aussi utilisé des lib de tests (Boost tests, google tests, Catch, …). Peut-être que tu pourrais ajouté une phrase pour décrire différentes solutions, et justifier la tienne.

Enfin, c’est une idée.

Bien cordialement.

+0 -0

Pour les tests de robustesse, cela m’évoque cette tendance où des gens écrivent des tests (U) pour voir comment des préconditions sont traitées. Cela me parait juste aberrant que de regarder comment réagit un programme à sqrt(-1), asin(2), memcpy(nullptr, nullptr, 1).

lmghs

Non, je ne parle pas de tester la lib Standard, ou le code des autres, mais de tester son code. Déjà, parfois on n’a pas le choix (Assurance qualité, logiciel qualifié ou certifié, haut niveau de disponibilité, …), et puis ça peut faire partie aussi pour se convaincre soit même, que l’on à bien pris en compte les cas hors norme dans son code et que ça plantera pas.

Dedeun

Mon exemple, ce n’était pas par rapport à la lib standard, c’était par rapport à des fonctions qui ont des narrow préconditions. J’ai pris pour exemple des fonctions de la lib standard, mais ça ne change rien à ma vision de la chose: tester le comportement des cas hors préconditions, je trouve ça ridicule. Si maintenant on est sur du wide (et plus du narrow), pourquoi pas de tester que les cas hors contrat soient bien attrapés. J’arrive vaguement à accepter la chose dans des contextes critiques, mais j’ai quand même beaucoup de mal à accepter la chose: j’appartiens à cette mouvance qui est persuadée qu’une erreur de programmation ne se rattrape pas car on ne peut pas savoir où la corruption a eu lieu. C’est le coup à détecter une anomalie et qu’à ne pas vouloir (/pouvoir) planter on sauvegarde/perdure avec un état corrompu.

Salut à tous,

Je reviens avec une mise à jour qui consiste à enlever la partie sur les invariants et l’encapsulation du premier chapitre, pour le mettre dans le deuxième. Ça permet de faire sur un seul chapitre les invariants, ce que c’est, les constructeurs pour les définir, explicit, etc. Le deuxième est toujours en rédaction, mais il va reprendre quasiment tout ce qu’il y avait dans l’existant.


Bonjour les agrumes !

La bêta a été mise à jour et décante sa pulpe à l’adresse suivante :

Merci d’avance pour vos commentaires.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte