La programmation en C++ moderne

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

a marqué ce sujet comme résolu.

Une petite question: Autant cela parait logique d’interdire la copie d’une classe à sémantique d’entité autant je ne comprend pas pourquoi interdire systématiquement le déplacement ?

Par exemple je me suis amusé à écrire une classe Socket que j’utilise ensuite dans une classe qui sert le serveur HTTP. Pour moi une socket est qq chose d’unique sur une machine, comme un fichier, donc sémantique d’entité. Pourtant j’ai besoin d’avoir la liste de tout les clients connectés à mon serveur, que je gère grâce à un vecteur de Socket. Si je supprime le déplacement d’une socket, je suis coincé, et allouer sur le tas des Sockets composées d’un int et d’un bool pour les gérer avec des handles c’est pas très efficace non ?

Salut !

Dans la pratique, le déplacement est surtout problématique pour les entités polymorphiques à cause du slicing.

Et pour le côté plus théorique, sémantique justement, par défaut je dirais que pour une "pure" sémantique d’entité, lorsqu’on a vraiment des objets uniques, cela n’a pas de sens de les déplacer. Une socket a une sémantique un peu particulière, c’est une sorte de handle finalement.

Enfin, dans ce cours on donne des conseils généraux pour des débutants, qui s’appliquent dans la plupart des cas, mais il y a des exceptions bien sûr !

+0 -0

J’avoue que cette partie est délicate à abordée, mais il ne faut pas non plus que le lecteur pense qu’il suffise de créer un constructeur et un opérateur de déplacement pour éviter toute copie.

Le déplacement n’a d’intérêt qu’avec les classes comportant des membres composés de parties dynamique. Dans la classe matrice, le constructeur de déplacement va appeler l’opérateur de déplacement du vecteur (car son contenu est alloué sur le tas) et c’est tout, les autres membres seront copiés. Si le vecteur avait été remplacé par un std::array<int,1000> par exemple (alloué sur la pile), rien ne serait déplacé mais copié.

Malheureusement, il me semble que vous présentez la pile et le tas dans le chapitre sur les handles, situé plus loin.

Note: Il me semble que l' "Optimisation de la valeur de retour" est garantie en C++ 17 (à vérifier).

Ce n’est absolument pas pour critiquer, c’est juste une réflexion, le travail d’écriture que vous faire est énorme, bravo.

+2 -0

Hello,

En l’absence de diff, je fais des passes que de temps à autres. Sorry. Voici la dernière. Pour l’instant: c’est super chouette.

Une première série sur les premiers pas. La suite après.

a- Question que je me pose § premier pas, définition séparée. Faut-il des parenthèses pour ceux qui viennent d’autres langages (Java, C#, Python), où ils ne séparent pas déclaration et définition? Si jamais ils sont tentés de faire une définition de fonction à l’intérieur de la définition de la classe, c’est possible. Et dans ce cas, on ne met pas NomClasse::.

b- §fonction libre

De la même manière que size est à la fonction une fonction membre des classes std::string et std::vector et à la fois une fonction libre

C’est "fois" que vous vouliez écrire, non?

c- Même chapitre. Autre question que je me pose: j’ai toujours plein d’idées de parenthèses, mais avec le risque d’alourdir => ne pas forcément toute les intégrer, loin de là. Pour le choix de pow en libre, on peut rajouter qu’en plus cela uniformise l’interface d’utilisation avec std::pow. Avoir Fraction{3,2}.pow(4) d’un côté et std::pow(1.5, 4) de l’autre serait bizarre.

d- §on ne fait pas d’exception

Quant à std::pow, aucune information n’est donnée. Nous la laisserons telle quelle.

Fichue Règle de Lakos. Vivement que cela change.

e- §Découpage en fichiers

Accessoirement, on ne peut pas mettre les définitions dans les .h sans rajouter inline

f- § exo du cercle:

std::sqrt(std::pow(lhs.x - rhs.x, 2) + std::pow(lhs.y - rhs.y, 2));

J’allais dire qu’il y a std::hypot, mais vu les mauvaises performances, j’ai arrêté de chercher à m’en servir. Quant à pow(x,2), les compilateurs optimisent, donc nickel! Je ne sais pas si c’était fait consciement.

Plus gênant par contre: M_PI n’est pas standard. Pffff. Et je vois que les arc fonctions trigo ne sont pas (encore) officiellement constexpr. Des pistes pour un pi portable: https://stackoverflow.com/questions/1727881/how-to-use-the-pi-constant-in-c/1727896

§Qui va construire

a- > ne vaille jamais nul.

La formulation sonne étrangement à mes oreilles. J’aurai dit "ne vaille jamais 0", ou "ne soit jamais nul".

b- Parenthèse sur les visibilités:

Au contraire, dans les fichiers sources, qui contiennent les détails d’implémentations, tout est privé et masqué. On ne peut pas y accéder depuis d’autres fichiers.

C’est très bizarre ce qui est dit je trouve. Caché oui. Privé. Non. On ne voit pas le code de l’implémentation. Certes, mais cela n’a que peu de rapport avec la notion de privé. Voir le code ou pas, c’est une autre problématique car private n’empêche pas de voir du code. Il empêche d’appeler du code. Et du code (non statique, ni dans un espace de noms anonymes) est exporté est donc appelable, même si on n’a pas défini de fichier d’entête. Il faudrait connaitre la signature.

Peut-être la parenthèse visait les types opaques comme std::FILE? Auquel cas OK, mais alors une question de formulation. La structuration de FILE est cachée et on ne peut pas y accéder: seule une abstraction est fournie. On peut cacher des fonctions aussi.

c- A propos de parenthèse, j’aurai tendance à indiquer que la notion d’encapsulation existe dans les autres langages OO mais qu’elle n’y est pas rendue toujours rendue de la même façon. Cf Eiffel ou Python.

d- § On cherche un constructeur

Si un des invariants est violé, alors on a une erreur, qu’on va signaler à l’utilisateur.

C’est au développeur qui est client du code de la fraction que l’on va signaler l’erreur en plantant son code (en attendant une vérification formelle de contrats). Il peut y avoir plusieurs couches de développeurs-clients, et éventuellement, un d’entre eux devra remonter à l’utilisateur qu’il n’est pas à même de poursuivre avec les données reçues — je ne parle pas de fraction ici: Cf mon exemple sur le message d’erreur de distance négative VS sqrt(nombre négatif) dans un de mes billets de blog sur la ppc.

e- § constructeur par défaut

Par contre, dans le cas des types natifs, rien n’est fait, leur valeur est indéfinie

Pas indéfinie mais indéterminée. Et je découvre même temps: b¤rd¤l il y a des différences de traitement entre unsigned char et les autres scalaires. Bref. non-déterminé ne veut pas forcément dire non défini, mais souvent. Et UB veut dire que le compilo pourrait un jour décréter que c’est unreachable (et élaguer massivement) vu qu’il n’y a pas de sens à faire tourner du code buggué…

Pour l’exemple, en changeant les options de compilations, on ne changerait pas la valeur affichée? Typiquement, sous VC++, le mode release ne force plus la 0-initialisation des trucs non initialisés.

f-

class Fraction
{
public:
   Fraction() = default;
}

Par soucis de clarté (et expérience sur le forum du sdz/oc), il faudrait soit remettre les attributs, soit remplacer l’accolade fermante (sans ;…) par '…'

g- § en toute amitié

Pour les setters, il y a un autre axe assez typique de certaines classes comme les fractions: les invariants multi-attributs. On peut introduire à ce niveau un nouvel invariant ici pour appuyer sur le fait que ce n’est pas adapté: tout fraction est garantie simplifiée. Du coup,

auto f = Fraction{3,2};
f.setNum(4);
f.setDen(3};

ne peut pas permettre de passer à 4/3. Quel que soit l’ordre d’appel des setters. Du coup pas d’autre choix que f = {4,3};

Sinon, la justification du retour mode donné sans abstraction est la bonne.

h-> il n’y a plus qu’à implémenter la fonction la même manière

de la même manière

Salut à tous.

Merci @bluescaster pour tes retours. Tu as eu une très bonne idée et le chapitre sur la sémantique de mouvement est moins magique avec des explications sur la pile et le tas. :)

@lmghs : merci pour tes retours. Je ne sais pas comment partager le diff. Il est disponible sur l’interface de rédaction, mais est-ce qu’il y a un moyen simple de le partager ? Au hasard, je ping @artragis qui pourra peut-être nous en dire plus.

Question que je me pose § premier pas, définition séparée. Faut-il des parenthèses pour ceux qui viennent d’autres langages (Java, C#, Python), où ils ne séparent pas déclaration et définition? Si jamais ils sont tentés de faire une définition de fonction à l’intérieur de la définition de la classe, c’est possible. Et dans ce cas, on ne met pas NomClasse::.

Oui c’est vrai que c’est possible. J’ai choisi de passer sous silence cette possibilité pour ne pas alourdir les explications. Après, si le lecteur vient de C# ou Java, espérons qu’il aura le réflexe d’aller se renseigner seul sur le pourquoi du comment. :)

Fichue Règle de Lakos. Vivement que cela change.

Je suis d’accord. J’étais étonné d’ailleurs en voyant que cette fonction n’était pas noexcept.

Plus gênant par contre: M_PI n’est pas standard. Pffff. Et je vois que les arc fonctions trigo ne sont pas (encore) officiellement constexpr.

Je vais corriger ce point alors. En attendant, cette définition fera l’affaire.

constexpr double pi { std::atan(1) * 4 };

Quant à pow(x,2), les compilateurs optimisent, donc nickel! Je ne sais pas si c’était fait consciemment.

Je n’en étais pas sûr, mais merci d’avoir confirmé ce que je pensais.

c- A propos de parenthèse, j’aurai tendance à indiquer que la notion d’encapsulation existe dans les autres langages OO mais qu’elle n’y est pas rendue toujours rendue de la même façon. Cf Eiffel ou Python.

Remarque intéressante. Je vais voir comment je pourrais en parler sans trop visuellement alourdir le cours, qui est déjà chargé de blocs information à cet endroit.

En tout cas, au programme de cette mise à jour, exceptés les remarques de @lmghs, tout se concentre dans le chapitre sur la sémantique de mouvement.

Merci d’avance à tous pour votre précieuse aide !


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.

Je viens de finir le chapitre sur la sémantique de mouvement, il est prêt à être relu par vos soins. J’espère ne pas avoir écrit de bêtise. :)

Merci d’avance à tous pour l’aide.

@informaticienzero


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,

J’ai réfléchi à un moyen d’expliquer simplement la mémoire et la différence entre copie et déplacement au moyen d’une analogie. Qu’en pensez-vous ?

Constructeurs et mémoire

Lorsque l’on exécute un programme, celui-ci se voit attribuer par l’OS une zone de mémoire pour allouer ses variables. Cette zone s’appelle la pile (Stack en anglais). Toute variable créée dans cette zone à une durée de vie correspondant à son « scope ».

int main()
{
    int a;
    …
    for (int i=0 ; i !=10 ; ++i) {
        int j;
        …
    } // i et j seront détruites ici, leur scope c’est le for
    …
} // a sera détruite ici, son scope c’est la fonction main

Cette partie de mémoire est très rapide d’accès, mais elle a 2 défauts. Sa taille est limitée à quelques Mo et le compilateur doit connaître la taille de toutes les variables que vous utiliserez dans chaque scope pour pouvoir générer le code machine de votre exécutable.

Heureusement il existe une seconde zone de mémoire que l’on appelle le tas (Heap ou Free Store en anglais). Sa taille correspond à toute la quantité de mémoire disponible sur votre PC qui n’est pas déjà attribuée à d’autres programmes.

Mais attention ! Pour allouer de la mémoire dans cette zone, il faut le demander à l’OS. Cette demande est assez compliquée à gérer correctement et il vaut mieux quand on est débutant (et même après) laisser les objets de la STL s’en charger.

Prenons le cas du type vecteur (std::vector<T>) qui est un container et faisons une analogie avec une entreprise de transport routier. Chaque vecteur représente un semi-remorque pouvant transporter des objets de type T.

Un semi-remorque est composé d’un tracteur (avec le moteur et la cabine) et d’une remorque accrochée à celui-ci. Tous les tracteurs sont "identiques" et les remorques peuvent avoir des tailles variables.

Lorsque l’on crée une variable de type vecteur, sans préciser sa taille, le constructeur par défaut est appelé.

std::vector<int> v;

Le vecteur crée le tracteur sur la pile, celui-ci à une taille fixe connu à la compilation, mais aucune remorque.

Lorsque on ajoute des données dans ce vecteur avec l’instruction

v.push_back(x);

le vecteur va demander à l’OS de lui allouer une remorque sur le tas pour pouvoir y ranger la donnée x, et va accrocher cette remorque au tracteur.

Au fur et à mesure des prochains push_back(), la remorque va se remplir. Lorsque celle-ci sera pleine, le vecteur demandera à l’OS une remorque plus grande, "copiera" le chargement d’une remorque à l’autre et demandera à l’OS de détruire la première. L’opération se répétera à chaque fois que la remorque sera pleine, tant qu’il reste de la mémoire de libre dans la machine.

Lorsque le vecteur sortira de son scope, la fin d’une fonction où il a été créé par exemple, sont destructeur sera appelé automatiquement et celui-ci demandera à l’OS de détruire la remorque en cours. Le tracteur lui sera automatiquement détruit puisque alloué sur la pile.

Voyons maintenant concrètement ce que cela donne avec la classe std::string, un container spécialisé dans le transport des caractères.

Le constructeur par défaut:

std::string s;

Alloue un tracteur sur la pile mais pas de remorque

Un constructeur avec argument:

std::string s{«Hello, »};

Alloue un tracteur s sur la pile et une remorque sur le tas capable de stocker « Hello, ». La chaine « Hello, » est alors copiée dans la remorque toute neuve.

Le constructeur de copie :

std::string s{«Hello, »);
std::string t{s};

Alloue un tracteur t sur la pile, une remorque aussi grande que celle de s sur le tas, et recopie le chargement de s dans la remorque de t. Les remorques de s et t ont donc un chargement identique mais ne sont pas attachées au même tracteur.

L’opérateur d’affectation par copie:

std::string s{“Hello,”};    // la remorque de s contient Hello,
std::string t{“World !”};   // la remorque de t contient World !
t = s ;

Dans un premier temps, détruit la remorque de t si elle n’est pas assez grande et en alloue nouvelle sur sur le tas. Ensuite, recopie le chargement de la remorque de s dans la remorque de t. Les 2 remorques contiennent « Hello, »

Maintenant, imaginons le cas suivant. On crée une fonction que concatène 2 chaines et renvoie le résultat par valeur:

std::string concat(const std::string& s1,const std::string& s2) // retourne s1 + s2
{
    auto tmp{s1};
    tmp += s2;
    return tmp; // recopie de tmp vers s
}

int main()
{
    std::string s{“Hello,”};
    std::string t{“World !”};
    ...
    s = concat(s , t);
    ...
}

Quand la fonction concat se termine, on va utiliser l’opérateur de copie. Le chargement de tmp sera copié dans la remorque de s juste avant que le camion tmp ne soit détruit. Quel gachi. Pourquoi recopier le chargement d’une remorque à l’autre si c’est pour détruire la première juste après ?

Depuis C++ 11 est apparu la notion de déplacement. Il s’effectue grâce au constructeur et à l’opérateur de déplacement qui sont appelés automatiquement si l’objet passé en argument, de se constructeur ou de cet opérateur, va être détruit juste après l’affectation. C’est bien le cas de tmp ici qui est la valeur de retour de la fonction.

On précise un type d’objet prêt à disparaître, en le faisant suivre de &&. La définition du constructeur de déplacement de std::string est donc:

std::string& std::string(std::string&&);

et son opérateur de déplacement:

std::string& std::string.operator=(std::string&&);

Revenons donc à notre code, sachant que l’on utilise un compilateur et une STL récente avec une classe std::string utilisant le déplacement.

std::string concat(const std::string& s1,const std::string& s2) // retourne s1 + s2
{
    auto tmp{s1};
    tmp += s2;
    return tmp; // deplacement de tmp vers s
}

int main()
{
    std::string s{“Hello,”};
    std::string t{“World !”};
    ...
    s = concat(s , t);
    ...
}

Au moment du return de la fonction concat, la remorque du camion s va être détruite. On va alors décrocher celle du tracteur de tmp pour l’accrocher au tracteur de s et tmp qui n’est plus composé que d’un tracteur seul va être détruit automatiquement puisque alloué sur la pile.

Le retour d’objet par valeur est donc très efficace depuis C++ 11, à condition que la classe de l’objet retourné définisse un constructeur et un opérateur de déplacement. C’est le cas de beaucoup de classes dans la STL.

Il existe aussi une fonction std::move(X) qui vas marqué l’objet passé en paramètre (ici X) comme étant prêt à disparaître. On peut donc forcé le déplacement d’une variable si le compilateur n’est pas à même de reconnaître une variable sur le point de disparaître.

Il même possible d’interdire la copie d’un objet en autorisant seulement son déplacement. On utilise cela par exemple lorsque l’on veut être sûr qu’il n’y ai pas plusieurs remorques identiques dans notre programme, pour reprendre notre analogie.

Note: Le language RUST, très à la mode, utilise par défaut le déplacement, c’est au programmeur d’indiquer quand il veut une copie, de même que toute les variables sont constantes par défaut à moins de préciser le contraire.

+2 -0

Je vais corriger ce point alors. En attendant, cette définition fera l’affaire.

constexpr double pi { std::atan(1) * 4 };

Malheureusement, aucune de ces fonctions n’est officiellement constepxr à ce jour :(


Au sujet du déplacement, je n’ai pas encore encore relu. La longue suggestion, que je met de côté pour l’instant, me fait penser à vous partager ce que je fais en live.

En live donc, je fais des dessins avec des flèches qui pointent vers des grosses patates histoire de commencer par faire comprendre le besoin. Ensuite, je passe un petit moment à faire comprendre la différence entre l’acte de déplacer (sur construction, affectation ou avec des fonctions qui finissent ainsi), et quelle signature est capable de matcher quoi: par valeur, &, const&, && VS variable locale, const ou non, résultat de fonction. Histoire de montrer que && n’implique pas forcément un déplacement, mais juste ce que cela est capable de matcher. Et je me rends compte que souvent j’ai déjà parlé de pile et de tas à ce moment.

Et après, c’est tous les détails arcaniques (syntaxe…)


Une classe de grande valeur

a- A la lecture, j’ai peur qu’une confusion surgisse. La sémantique de valeur, c’est sémantiquement la comparaison, et la possibilité de dupliquer. OK.

Après, la surcharge des opérateurs, les relations d’ordres, c’est des sous-ensembles. Ici, on définit un sous-modèle avec sa sémantique qui repose sur la sémantique de valeur (et son modèle — à la C++20 quelque part). C’est ce qui est fait dans le chapitre: on explore un sous ensemble d’opérations que l’on rencontre souvent, mais pas systématiquement, dans la sémantique de valeur. Alors, pas besoin d’utiliser les mots barbares que j’ai employés, mais je pense qu’il est important d’être clair. Genre "nous allons explorer un sous-ensemble des classes ayant une sémantique de valeur" (trouver une meilleure formulation). Et après, roulez jeunesse!

C’est histoire de bien poser ce qui va être abordé, et teaser à la fin: "nous verrons plus loin le cas de classes à sémantique de valeur pour lesquelles il est nécessaire de définir constructeur de copie et opérateur d’afffectation par copie."

Eventuellement aussi indiquer que l’on segmente dans ce cours car les recettes typiques de chaque sémantique varie, et que nous faisons le choix de partager notre expérience pour non pas présenter la syntaxe du C++, mais de présenter comment écrire tel ou tel type de classe selon leurs propriétés.

b- Les 3 propriétés de l’égalité, c’est des propriétés mathématiques pour moi. J’image que le choix de ne pas donner les termes officiels (réflexivité, symétrie et transitivité) est volontaire?

c- <=>

Je crois que j’avais parlé de mon expérience avec la dernière version du tuto du sdz qui était sortie un chouilla avant la version finale du C++11 et que là, parler des divers opérateurs de comparaison sans évoquer le spaceship operator me fait le même effet.

d- "Les opérateurs d’affectation intégrés", ce n’est pas "composé" la traduction la plus "officielle"?

e- Le premier include de fraction.cpp devrait être fraction.hpp. Toujours: c’est pour s’assurer que le fichier d’en-tête livré est auto-suffisant et que le client n’aura pas à galérer et à nous traiter de boulets amateurs et autres noms d’oiseaux.

f- > @exception Lance une std::invalid_argument si la chaîne n'est pas correcte.

La syntaxe doxygen est @exception TYPE explication/condition

g- std::invalid_error est une std::logic_error, c’est fait pour faire de la programmation défensive: pas pour décoder une chaine fournie par l’utilisateur final, un fichier…


Entrons dans la matrice

h- Idéalement, c’est pas <iostream>, mais <iosfwd> qu’il faudrait dans le .hpp non template.

i- return m_matrice[y * m_nb_lignes + x];

Côté bonnes pratiques je recommande à chaque fois de définir une fonction interne offset qui factorise assertions et calculs qui sont dupliqués dans deux fonctions.

j- > indexs

C’est invariable: https://fr.wiktionary.org/wiki/index

k- Pour +=, idéalement, cela se fait en une seule boucle.

l- Côté test, je demande aussi à mes élèves de faire en sorte que le code std::cout << (m1 + m2 + m2); compile.

C’est tout con, cela exige que op<< prenne une référence constante (ou une copie…), et donc que op() ait une version const, que les getters soient bien const. Et comme on utilise m(i,i) = i, cela demande aussi l’opérateur () non const. Avec un qui renvoie de la référence constante et l’autre non.

Bref. C’est un premier contact avec le fait que seuls les passages par valeur ou par référence constante peuvent accepter des résultats de fonctions, i.e. des expressions, IOW des rvalues.

C’est un peu la 2e/3e lame Gilette qui choppe les fainéants qui s’en sortaient jusqu’à lors sans code const-correct.


Demain je me mouve

Merci @lmhgs pour tes retours.

a- A la lecture, j’ai peur qu’une confusion surgisse. La sémantique de valeur, c’est sémantiquement la comparaison, et la possibilité de dupliquer. OK.

lmghs

J’ai rajouté un petit encart informatif dans la partie sur la surcharge pour énoncer ça.

b- Les 3 propriétés de l’égalité, c’est des propriétés mathématiques pour moi. J’image que le choix de ne pas donner les termes officiels (réflexivité, symétrie et transitivité) est volontaire?

lmghs

Non, involontaire, car je ne me rappelais tout simplement plus de ces termes et je ne voulais pas écrire n’importe quoi. C’est ajouté.

c- <=>

Je crois que j’avais parlé de mon expérience avec la dernière version du tuto du sdz qui était sortie un chouilla avant la version finale du C++11 et que là, parler des divers opérateurs de comparaison sans évoquer le spaceship operator me fait le même effet.

lmghs

Je ne me souviens pas de cette discussion, mais ma mémoire est loin d’être infaillible. De toute façon, on doit refaire une passe globale sur tout le cours pour commencer à parler de C++20.

d- "Les opérateurs d’affectation intégrés", ce n’est pas "composé" la traduction la plus "officielle"?

lmghs

J’avoue ne pas avoir d’idée. La FAQ C++ de Développez n’en parle pas, j’ai déjà lu « opérateurs internes » ou encore « opérateurs raccourcis ». Il faudrait que je jette un œil à un bon ouvrage français pour voir.

e- Le premier include de fraction.cpp devrait être fraction.hpp. Toujours: c’est pour s’assurer que le fichier d’en-tête livré est auto-suffisant et que le client n’aura pas à galérer et à nous traiter de boulets amateurs et autres noms d’oiseaux.

lmghs

Que veux-tu dire par auto-suffisant ? Tu as déjà rencontré une erreur de ce genre ?

g- std::invalid_error est une std::logic_error, c’est fait pour faire de la programmation défensive: pas pour décoder une chaine fournie par l’utilisateur final, un fichier…

lmghs

Effectivement, je n’ai pas pensé à vérifier. Du coup, un runtime_exception ira sans doute mieux.

h- Idéalement, c’est pas <iostream>, mais <iosfwd> qu’il faudrait dans le .hpp non template.

lmghs

Je savais bien que <iostream> ne convenait pas et que tu avais déjà fait la remarque, mais impossible de retrouver l’en-tête qui convient mieux.

i- return m_matrice[y * m_nb_lignes + x];

Côté bonnes pratiques je recommande à chaque fois de définir une fonction interne offset qui factorise assertions et calculs qui sont dupliqués dans deux fonctions.

lmghs

Effectivement. Mais si on veut utiliser une seule fonction, y’aura forcément un const_cast quelque part, je me trompe ? Il faudra alors l’introduire, pour ne pas prendre 99% des lecteurs par surprise.

l- Côté test, je demande aussi à mes élèves de faire en sorte que le code std::cout << (m1 + m2 + m2); compile.

C’est tout con, cela exige que op<< prenne une référence constante (ou une copie…), et donc que op() ait une version const, que les getters soient bien const. Et comme on utilise m(i,i) = i, cela demande aussi l’opérateur () non const. Avec un qui renvoie de la référence constante et l’autre non.

Bref. C’est un premier contact avec le fait que seuls les passages par valeur ou par référence constante peuvent accepter des résultats de fonctions, i.e. des expressions, IOW des rvalues.

C’est un peu la 2e/3e lame Gilette qui choppe les fainéants qui s’en sortaient jusqu’à lors sans code const-correct.

lmghs

Bonne idée, je vais intégrer ça.

Demain je me mouve

lmghs

Merci d’avance. :)


@bluescaster : merci d’avoir pris le temps d’écrire cet exemple. C’est vrai que ça peut aider à mieux comprendre l’intérêt du déplacement.

Par contre, pour std::string, même si on ne peut être 100% sûrs, il y a des chances qu’en interne l’implémentation utilise la Small String Optimisation et qu’on ne voit donc aucun gain à déplacer, car sur la pile. :)

Salut, c’est vrai pour la Small String, on peut imaginer que le chauffeur emmène les petites chaîne avec lui dans la cabine du camion :D .

Par contre j’ai découvert en faisant qq essais hier que si on déclare le constructeur et l’opérateur de déplacement =delete le return d’un objet temporaire par valeur ne compile plus du tout ! Je pensais que le compilateur se rabattait sur le constructeur ou l’opérateur de copie.

+0 -0

Salut ! C’est parce que quand tu supprimes le constructeur et l’opérateur d’affectation d’affectation par déplacement, le compilateur ne génère aucune autre fonction qu’il génère normalement par défaut ! Si tu veux garder la copie et pas le déplacement, il suffit de la déclarer explicitement :

struct NonMovable
{
    // Désactive la génération automatique implicite des autres membres du Big Five :
    NonMovable(NonMovable&&) = delete;
    NonMovable& operator=(NonMovable&&) = delete;

    // On demande explicitement la génération au compilateur :
    NonMovable(NonMovable const&) = default;
    NonMovable& operator=(NonMovable const&) = default;

    NonMovable() = default;
};
+0 -0

Salut, c’est ce que j’ai fait:

class Toto {
public:
        Toto() = default;
        Toto(const Toto&) = default;
        Toto& operator=(const Toto&) = default;
        Toto(Toto&&) = delete;
        Toto& operator=(Toto&&) = delete;
        ~Toto() = default;
private:
        int m_x{0};
};

Toto f() {
        Toto tmp;
        return tmp;
}

int main()
{
        Toto t = f();
}

g++ 7.4.0 me renvoie:

main.cpp: Dans la fonction « Toto f() »:
main.cpp:15:9: error: utilisation de la fonction supprimée « Toto::Toto(Toto&&) »
  return tmp;
         ^~~
main.cpp:6:2: note: déclaré ici
  Toto(Toto&&) = delete;
  ^~~~
main.cpp: Dans la fonction « int main() »:
main.cpp:20:13: error: utilisation de la fonction supprimée « Toto::Toto(Toto&&) »
  Toto t = f();
             ^
main.cpp:6:2: note: déclaré ici
  Toto(Toto&&) = delete;
  ^~~~

A priori, il faut mettre le constructeur et l’opérateur de déplacement en commentaires (donc ne pas les citer du tout) pour que cela passe !

+0 -0

Merci @lmhgs pour tes retours.

Avec plaisir.

e- Le premier include de fraction.cpp devrait être fraction.hpp. Toujours: c’est pour s’assurer que le fichier d’en-tête livré est auto-suffisant et que le client n’aura pas à galérer et à nous traiter de boulets amateurs et autres noms d’oiseaux.

lmghs

Que veux-tu dire par auto-suffisant ? Tu as déjà rencontré une erreur de ce genre ?

Ohhh oui.

// toto.hpp
#ifndef TOTO_HPP__
#define TOTO_HPP__

class Toto {
    std::string s;
};

#endif

// toto.cpp
#include <string>
#include "toto.hpp"
...


// client.cpp
#include "toto.hpp" // KABOOM
#include <string>

Ca rejoint https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rs-contained

h- Idéalement, c’est pas <iostream>, mais <iosfwd> qu’il faudrait dans le .hpp non template.

lmghs

Je savais bien que <iostream> ne convenait pas et que tu avais déjà fait la remarque, mais impossible de retrouver l’en-tête qui convient mieux.

  • iosfwd: déclarations:
  • ostream et istream: définition de std::ostream et std::istream
  • iostream: définitions des 4 globales: std::cout, etc

Le dernier a l’avantage d’être newb' friendly, mais pas temps de compilation friendly

i- return m_matrice[y * m_nb_lignes + x];

Côté bonnes pratiques je recommande à chaque fois de définir une fonction interne offset qui factorise assertions et calculs qui sont dupliqués dans deux fonctions.

lmghs

Effectivement. Mais si on veut utiliser une seule fonction, y’aura forcément un const_cast quelque part, je me trompe ? Il faudra alors l’introduire, pour ne pas prendre 99% des lecteurs par surprise.

Plus simple:

T& operator[](size_t l, size_t c) {
    return m_buffer[offset(l,c)];
}

T const& operator[](size_t l, size_t c) const {
    return m_buffer[offset(l,c)];
}

private:

// C'est ça que l'on factorize
// Du coup, on est un peu plus DRY, et vu que cela sera très probablement inliné...
// ça ne coûtera rien
std::size_t offset(size_t l, size_t c) const noexcept {
    assert(l < L());
    assert(c < C());
    return l * C() + c;
}

@bluescaster : merci d’avoir pris le temps d’écrire cet exemple. C’est vrai que ça peut aider à mieux comprendre l’intérêt du déplacement.

Par contre, pour std::string, même si on ne peut être 100% sûrs, il y a des chances qu’en interne l’implémentation utilise la Small String Optimisation et qu’on ne voit donc aucun gain à déplacer, car sur la pile. :)

informaticienzero

Avec un vecteur c’est bon :) (j’imagine que c’était le sens de la remarque)


PS: un handle n’a pas nécessairement une sémantique de valeur, cf. fstream, unique_ptr… C’est un autre axe.

Le code n’est valable qu’a partir de C++ 17. Le compilateur gcc 9.3 ne compile par en C++17 par défaut à priori.

(/home/J0039644/src/zeste)===> gdb -quiet ./a
Reading symbols from ./a...done.
(gdb) br main
Breakpoint 1 at 0x1004010a5: file main.cpp, line 20.
(gdb) run
Starting program: /home/J0039644/src/zeste/a
[New Thread 6268.0x461c]
[New Thread 6268.0x432c]
[New Thread 6268.0x51e8]
[New Thread 6268.0x5f78]
[New Thread 6268.0x1cc4]

Thread 1 "a" hit Breakpoint 1, main () at main.cpp:20
20              Toto t = f();
(gdb) info source
Current source file is main.cpp
Compilation directory is /home/J0039644/src/zeste
Located in /home/J0039644/src/zeste/main.cpp
Contains 21 lines.
Source language is c++.
Producer is GNU C++14 9.3.0 -mtune=generic -march=x86-64 -g.
Compiled with DWARF 2 debugging format.
Does not include preprocessor macro info.
+0 -0
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