La programmation en C++ moderne

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

a marqué ce sujet comme résolu.

Reprise du dernier message de la page précédente

Salut à tous.

Je viens de mettre à jour le chapitre sur les handles. Je continue toujours à réfléchir à la sémantique de mouvement.


Bonjour les agrumes !

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

Merci d’avance pour vos commentaires.

Édité par informaticienzero

Salut à tous.

Je viens d’écrire ce petit paragraphe que je vois prendre ça place dans le chapitre sur les handles, dans la partie Qui est le propriétaire ?, afin d’introduire la notion mais sans parler d’optimisations, ce qui est un thème plus avancé je suis d’accord.

Qu’en-pensez-vous ? Le trouvez-vous clair ? A t-il sa place dans ce chapitre ?


Transmission de propriété

Le transfert de responsabilité est nécessaire quand on manipule des std::unique_ptr car la copie est impossible. Mais en interne, que se passe t-il quand on fait appel à std::move? C’est ce que nous allons voir.

Rvalues et rvalue-references

En C++, il existe, entre autres, les lvalues et les rvalues.

  • Les rvalues, acronymes de right-handed values, désignent ce qu’on va trouver à droite de l’opérateur d’affectation =. On retrouve dans cette catégorie les littéraux, comme 3 ou -8.71, mais aussi les objets qui arrivent en fin de vie, comme le temporaire m1 + m2 dans un expression comme int m3 { m1 + m2 };.
  • Les lvalues, acronymes de left-handed values, désignent (grossièrement) une expression qui peut se trouver à gauche de l’opérateur d’affectation =, tout ce qui n’est pas une rvalue.
Règles exactes

En vrai, les règles sont plus subtiles que ça. Je ne vous ai donné qu’une version incomplète, mais qui suffit largement pour comprendre ce chapitre. Quand vous aurez terminé ce cours, vous aurez le niveau pour aller chercher seuls les explications complètes.

Et tout comme il existe des références sur des lvalues, qu’on créé avec l’esperluette `&, il existe aussi des références sur des rvalues, nommées en anglais rvalues references.

Les rvalues references permettent donc de passer en argument des rvalues, ce qui était impossible jusqu’en C++11, car nous ne disposions que de lvalues references. L’exemple suivant, fourni par @lmghs, illustre cette nouvelle possibilité.

#include <iostream>
#include <string>

std::string source() noexcept
{
    return "src";
}

void f(std::string & s) noexcept
{
    std::cout << "f(&)\n";
}

void f(std::string && s) noexcept
{
    std::cout << "f(&&)\n";
}

int main()
{
    // Ici, varloc est une lvalue.
    std::string varloc { "vl" };
    std::cout << "f(varloc) -> ";
    f(varloc);

    // Au contraire, le résultat de source() est une rvalue.
    std::cout << "f(source()) -> ";
    f(source());
   
    return 0;
}
f(varloc) -> f(&)
f(source()) -> f(&&)

Sans la surcharge prenant une rvalue reference, le code ne compile tout simplement pas.

[Visual Studio]
Erreur C2664 'void f(std::string &)' : impossible de convertir l'argument 1 de 'std::string' en 'std::string &'

------------------------------------------------------------

[GCC]
prog.cc: In function 'int main()':
prog.cc:21:13: error: cannot bind non-const lvalue reference of type 'std::string&' {aka 'std::__cxx11::basic_string<char>&'} to an rvalue of type 'std::string' {aka 'std::__cxx11::basic_string<char>'}
   21 |     f(source());
      |       ~~~~~~^~
prog.cc:9:22: note:   initializing argument 1 of 'void f(std::string&)'
    9 | void f(std::string & s)
      |        ~~~~~~~~~~~~~~^

------------------------------------------------------------

[Clang]
prog.cc:21:5: error: no matching function for call to 'f'
    f(source());
    ^
prog.cc:9:6: note: candidate function not viable: expects an l-value for 1st argument
void f(std::string & s)
     ^
1 error generated.

std::move

Mais quel est le rapport entre les rvalues references et std::move ? En fait, cette denrière porte mal son nom, car en interne, elle se contente de transformer ce qu’on lui passe en rvalue, en utilisant static_cast. C’est ce qu’illustre le code ci-dessous.

#include <iostream>
#include <string>

int main()
{
    std::string varloc { "vl" };
    std::cout << "f(varloc) -> ";
    // On utilise std::move, ce qui provoque un appel à f(&&).
    f(std::move(varloc));

    std::cout << "f(source()) -> ";
    f(source());
    return 0;
}
f(varloc) -> f(&&)
f(source()) -> f(&&)

Mais alors comment le transfert de propriété se fait-il ? C’est là que la sémantique de mouvement entre en jeu.

Sémantique de mouvement

Contrairement à la copie qui garde l’objet original en l’état, le mouvement le « vampirise » dans le sens que **tous les attributs de l’objet déplacé sont comme « arrachés » du premier et donnés tels quels au deuxième. Avec un exemple (merci @lmghs), vous allez mieux comprendre.

Imaginons que vous vouliez me transmettre un document très important, enfermé dans une mallette. Comment allons-nous faire ? Soit j’achète une mallette tout à fait semblable à la vôtre, je recopie le document à transmettre à la virgule près eton détruise votre mallette une fois fini, ce qui correspond à une copie. Soit vous me donnez la mallette directement, avec son contenu qui change simplement de propriétaire. C’est le déplacement.

Pour qu’un type puisque être déplacé (on dit en anglais movable), il faut qu’il implémente un constructeur par déplacement et un opérateur d’affectation par déplacement. Et c’est justement ce que fait std::unique_ptr. Donc quand on fait pointeur_1 = std::move(pointeur_2), on fait appel à l’opérateur d’affectation par déplacement et c’est dedans qu’est réalisé la « vampirisation » de l’objet paramètre et de ce fait le transfert de propriété.

Et que devient l’objet une fois vidé de ses attributs ? Qu’en fait-on ?

La norme garantit simplement que l’objet est valide, c’est-à-dire qu’il ne risque pas de provoquer de crash ou autre, mais son contenu est indéfini. Il est peut-être inchangé, ou bien vide, ou bien peut contenir les codes de lancement d’un missile nucléaire. En fait, la seule chose qu’on peut faire avec un objet qui a été déplacé, c’est lui affecter une nouvelle valeur.

Ne pas réutiliser un objet après std::move

Utiliser un objet qui a été déplacé est un comportement indéterminé, en plus de ne pas avoir de sens d’un point de vue logique.

Et pour mes types ?

Il est bien entendu possible de rendre ses propres types déplaçables, ou bien à l’opposé décider d’interdire le déplacement. Il suffit de demander au compilateur de le faire pour nous. En effet, dès lors qu’on demande ou interdit explicitement le constructeur et l’opérateur d’affectation par recopie, le compilateur ne génère plus ceux par déplacement.

class A
{
public:
    A(A&& mouvement) noexcept  = default;
    A& operator=(A&& mouvement) noexcept = default;
}

class B
{
public:
    B(B&& mouvement) noexcept  = delete;
    B& operator=(B&& mouvement) noexcept = delete;
}

Contrairement à la copie qui est toujours désactivée avec la sémantique d’entité, on peut choisir de garder ou non le déplacement. Cela dépendra de la sémantique que vous souhaitez donner à votre classe. Autant déplacer un handle vers une ressource fait sens, autant vouloir déplacer les attributs d’une lampe vers une autre n’en fait pas. C’est donc du cas par cas.


PS: je compte aussi rajouter une petite partie De l'importance de prendre ses responsabilités pour parler des deux notions soulevées par @gbdivers. Elle se trouve ci-dessous.


Ce chapitre a du paraitre difficile pour certains et c’est normal. Beaucoup de nouvelles notions ont été introduites, qui sont pour quelques-unes spécifiques à C++. Il est néanmoins important que nous en parlions, car cela à un rapport avec la qualité logicielle.

Quand nous écrivons des programmes, nous voulons que ceux-ci soient de qualités et aient le moins de bugs possibles. Cela passe notamment par le fait que les objets que nous manipulons soient valides. Ainsi, on ne veut pas manipuler un objet qui n’est pas encore initialisé, ou bien déjà détruit. De même, on ne veut pas libérer la mémoire si nous ne sommes pas le propriétaire de la ressource. Il faut donc maitriser deux notions.

  • L'ownership, c’est-à-dire qui est responsable de la ressource et de sa libération.
  • La durée de vie des objets, c’est-à-dire quand l’objet est créé et quand il est détruit.

Dans beaucoup de langages, cette complexité est masquée, mais pas en C++. D’où l’importance des outils que nous avons découvert dans ce chapitre, à savoir RAII et les handles. Grâce à eux, nous pouvons écrire du code plus résistant et plus explicite, puisque les durées de vies et les responsabilités des ressources sont clairement définies.

Édité par informaticienzero

Voila

II-8.2.5. Vérifier si le tableau est vide

Ca doit forcement etre con. Mais j’ai vu ce qu’etais les exception maintenant. Pourquoi pop_back() ne jetterais pas une exception quand il opere sur une chaine ou un tableau vide? C’est pareil pour les acces en dehors du tableau. Pourquoi partout des UB? J’ai meme verifier j’avais du mal a y croire

III-13.0.0.2. Mais de quel type es-tu ?

Pour renvoier une valeur…

c’est "renvoyer" je penses ici.

III-13.2.5. Un mot sur la déduction de type

Certains reprochent son côté « rigide » à ce mot-clé. En effet, auto seul dégage la référence. À l’inverse, auto & sera toujours une référence. Et il en est de même pour const : auto const sera toujours constant et auto const & sera toujours une référence sur un objet constant.

ça doit certainement venir de moi mes "dégage" ici me parait un peu obscur. C’est très bien explique ensuite et exemple a l’appui en plus, mais quand je fais la relecture pour la 3 eme fois et que j’arrive toujours a penser dans un premier temps que quelque chose comme 'auto var { refint }' signifie var est une int reference

ici (en résumé) ça me semble limpide

Le mot-clef auto ne conserve pas les références, mais le nouveau mot-clef que nous avons introduit, decltype , si.

III-13.5.1. Le problème

Il manque le cas terminal pour l’algorithme est_impair. J’ai l’impression que c’est fait expres. ca me fait penser a ce que tu avais fait de std::cin.ignore( ) et std::numeric_limits<std::streamsize>::max() en hardcodant d’abord la valeur max a 255.

III-15.. Le typage

14.2.1. Contrats assurés par le compilateur 15.Le typage

Il manque du texte entre les deux. Enfin c’est ce que j’ai d’abord cru. C’est le pdf qui est genere qui a comme un bug sur la numerotation des titres. J’imagine que je suis un peu hors sujet.

III-15.2.4. Les tests unitaires et les postconditions

Rappelez-vous, nous avons défini les postconditions comme étant les contrats que doit respecter une fonction si ses préconditions le sont. Nous pouvons, grâce aux tests unitaires, vérifier que la fonction fait bien son travail en lui fournissant des paramètres corrects et en vérifiant que les résultats sont valides et correspondent à ce qui est attendu.

Est ce que les tests qu’on a effectues juste avant cette remarque verifient des posconditions ( c’est l’impression que j’ai ).

    void test_push_back_taille_augmentee ( ) {

            std::vector<int> tableau { 1, 2, 3 };
            assert ( std::size ( tableau ) == 3 && "La taille doit être de 3 avant l'insertion." );
            tableau.push_back ( -78 );
            assert ( std::size ( tableau ) == 4 && "La taille doit être de 4 après l'insertion." );
    }

    void test_push_back_element_bonne_position ( ) {

            std::vector<int> tableau { 1, 2, 3, 4 };
            int const element { 1 };
            tableau.push_back ( element );
            assert ( tableau [ std::size ( tableau ) - 1 ] == element && "Le dernier élément doit être 1." );
    }

Le cas echeant, ce serait bien que ce soit souligne ici je crois. On pourrait avoir par exemple:

    void test_push_back_element_bonne_position ( ) {

            //[precondition] etant donnee ...
            std::vector<int> tableau { 1, 2, 3, 4 };
            int const element { 1 };
            //[code evalue] Lorsque ...
            tableau.push_back ( element );
            //[poscondition(Resultat attendu)] on verifie que push_back() ...
            assert ( tableau [ std::size ( tableau ) - 1 ] == element && "Le dernier élément doit être 1." );
    }

Mais je comprendrais si on me répondait que vu le chemin parcouru jusqu’ici on ne va plus autant se faire tenir par la main que durant le premier chapitre

III-15.5. [T.P] Gérer les erreurs d’entrée — Partie V

Indice N’oubliez pas que s’il y a fermeture de flux, il faudra lancer une exception puisque la lambda sera incapable de vérifier la postcondition « affecter une valeur correcte fournie par le flux à la variable ».

La lambda!?? Comme dans III-16. Des fonctions somme toute lambdas?

###Contenu masqué n°37 : Correction

C’est pas juste un contenue masqué. C’est la correction du TP Partie V.

III-16.2.1. Quelques exemples simples

std::for_each(std::begin(chaines), std::end(chaines),
          [](std::string const & message) -> void
{
    std::cout << "Message reçu : " << message << std::endl;
});

L’exemple précédent montre une lambda qui ne prend qu’un paramètre. En effet,std::for_each attend un prédicat unaire, c’est-à-dire que l’algorithme travaille sur un seul élément à la fois. La lambda ne prend donc qu’un seul paramètre.

La lambda renvoie void.

Un prédicat est une expression qui prend, ou non, des arguments et renvoie un booléen.

Par contre std::sort attend bien un predicat binaire mais cette lambda assure renvoyer un double

std::sort(std::begin(tableau), std::end(tableau),
    [](double a, double b) -> double
{
    return a > b;
});

int const diviseur { 2 };

III-16.8.2. Capture par référence

int const diviseur { 2 };
int somme { 0 };
// Il n'y a aucun problème à mélanger les captures par valeur
et par référence.
std::for_each(std::cbegin(nombres), std::cend(nombres),
[diviseur, &somme](int element) -> void
{
    if (element % diviseur == 0)
{
    somme += element;
}

On dirait que les lambda ont été conçues exprès pour encourager a ne pas accéder a l’environnement externe. Pourquoi fait-on ça aussi souvent?

III-17.1. Quel beau modèle!

On peut faire encore mieux : on se fiche du type réel du conteneur, du moment qu’il est itérable. Essayons d’appliquer ce principe et d’écrire une fonction afficher qui marche avec n’importe quel type de conteneur itérable.

… et dont chaque element e est affichable.

la e est toujours passé par référence. c’est contre la culture C++ qui consiste a passer toujours les types primitifs par valeur mais j’imagine que la culture n’est rien face au gain et qu’un trop d’opti vaut mieux que pas du tout.

III-17.1.1. La bibliothèque standard : une fourmilière de modèles

Des structures de données.

En fait, on a vu et utilisé un nombre incalculable de fonctions génériques dans la bibliothèque standard : celles renvoyant des itérateurs ( std::begin , std::end , etc), les conteneurs, les algorithmes, etc.

Est ce que les conteneurs sont des fonctions aussi? ou alors tu parles de fonctions dans le sens de fonctionnalités.

Les tableaux, en informatique, font partie de la grande famille des structures de données . Ces structures de données sont des façons particulières d’organiser et de stocker des données. Il y a ainsi des structures pour stocker un nombre fixe de données du même type, d’autres pour un nombre variable, d’autre encore pour stocker des données de types différents. Ces objets, conçus spécialement pour stocker des données, sont appelés conteneurs.

###Contenu masqué n°42 : Correction T.P partie VIII Il n’y pas de prédicat(je veux dire que je n’en vois pas dans l’entete template). Les lignes …

else
{
    std::cout << "Le prédicat n'est pas respecté !" << '\n');
    std::endl;
}

et aussi …

sont en trop dans la fonction.

III-18.2. std::tuple hétérogène

std::tuple tuple1 { 2, "bool"s, "sup"s, "Numeric"s };
auto tuple2 = std::make_tuple ( 2, "bool"s, "sup"s, "Numeric"s );

Du coup std::make_tuple c’est un cadavre? Moi ca m’a l’air de faire doublon en plus d’etre moins confortable.C’est le truc avec l’inference des types template; pour moi ca suicide tous les helpers. Je soupconne que je sois certainement un peu naif.

III-18.2.4. Équations horaires

if (std::get<1>(coordonnees) < 0)
{
    // On ne continue plus la simulation dès que y est au
    niveau du sol.
    break;
}

Sans vouloir en faire un cour de physique, en considérant bien sur que le lancement se fasse depuis le sol. On pourrait preciser dans l’ennonce.

Vous voulez un exemple concret d’utilisation des std::tuple ? Vous aimez la physique ? Vous allez être servi. Nous allons coder une fonction retournant les coordonnées (x; y) d’un objet lancé à une vitesse v 0 et à un angle α, en fonction du temps t.

… d’un objet lancé depuis le sol à une vitesse v 0 et à un angle α, en fonction du temps t.

III-18.6. Une longue énumération

On s’en sert typiquement pour limiter le champs des possibilités pour un type. Ainsi, au lieu de stocker le jour de la semaine sous forme de std::string , ce qui n’offre pas de protection si l’utilisateur rentre n’importe quoi, on va utiliser une énumération. Ainsi, celui-ci n’a pas d’autres choix que d’utiliser une des valeurs que nous avons nous-mêmes définis. C’est beaucoup plus souple que de devoir faire plein de vérifications sur une chaîne de caractères.

J' ai plutôt l’impression que c’est le programmeur qui est contraint par ses valeurs limites. Tu dis un peu ça ensuite je pense. Si je trompe veillez toujours a me remettre sur le droit chemin :)

III-18.6.1. En résumé ( 18.6. Une longue énumération )

Grâce aux tuples et aux structures, nous sommes en mesure de retourner plusieurs variables d’un coup.

Plusieurs variables de type differents types! Ici(Distributeur d’argent) on avait retourne un vecteur

Contenu masqué n°43 : Correction

Tu utilises la concaténation que tu n’as pas encore expliqué à ce stade du cours.

III-19.2.2. Et avant, on faisait comment?

Un autre avantage, c’est de pouvoir réutiliser une même variable plusieurs fois, alors que la décomposition C++17 ne nous le permet pas.

Je ne comprends pas bien j’ai essaye d’utiliser une variable de strutured binding plusieurs fois ça a marché.

Et puis pour moi le comité est parfois lent un peu. On aurait pu facilement faire (moi ça me saute aux yeux):

[ nom, void, age ]  = std::tuple { "kill", "Bill", 24 };//ou
[ nom, _, age ]     = std::tuple { "kill", "Bill", 24 };//mais je prefere le premier

et puis comme ça on bute std::tie et std::ignore. Tranquille.

III-19.3.4 La surcharge des operateurs (Juste une remarque je sais pas ce qu’on en fera)

C’est en découvrant que les opérateurs de flux étaient binaires et qu’il ne font que faire "une donnée D sur un flux F égal un flux F" (en modifiant le flux F et certainement 2, 3 trucs en interne mais j’imagine qu’on s’en fout ) que j’ai compris pourquoi cette notation était possible.

Avant ça j’avais un peu l’impression (je savais que c’était pas possible hein) que c’était une construction particulière que le langage autorisait. Si j’avais sérieusement considéré que c’était juste une écriture particulière et non un assemblage cohérent de brique logique préexistante (soit déjà vu dans le cours, soit un peu obscur) je me serais découragé. Voila juste pour faire part de ce qu’il y a parfois des moments ou j’étais un peu beaucoup inquiet pendant mon évolution sur ce cours. C’est vrai qu’on nous dit de relire pour bien assimiler mais c’est vrai aussi que parfois juste avancé et découvrir le "plus" permet d’arriver a comprendre le "moins".

20.Les opérateurs arithmétiques

On peut faire encore mieux non?

Avec le support de operator<<

Mais je comprends que faire tout en même temps ça puisse un peu embrouiller

// On vérifie que a et b sont égaux.
bool operator==(Fractionconst& a, Fractionconst& b)
{
    Fractionconstgauche { a.numerateur* b.denominateur,a.denominateur* b.denominateur };
    Fractionconstdroite { b.numerateur* a.denominateur,b.denominateur* a.denominateur };
    return gauche.numerateur== droite.numerateur && gauche.denominateur== droite.denominateur;
}

On peut difficilement faire plus verbeux. La encore je me trompe peut-être. J’ai personne a cote de moi pour vérifier si je dis une connerie ou ne serait-ce qu’en discuter. Mieux(plus cour et plus lisible):

bool operator==(Fractionconst& a, Fractionconst& b)
{
    return { b.numerateur* a.denominateur ==  a.numerateur * b.denominateur };
}

Il faudrait peut être aussi expliqué pourquoi la solution naïve qui consisterait a évaluer l’égalité des division des deux membres est trop fragile. Je pense en particulier a la représentation des nombres en mémoire et aussi au nombre irrationnels. Et si je dis une bêtise une fois de plus, soyez attentif a me corriger.

Édité par mougnolribama

in medio stat virtus

+1 -0

Ca doit forcement etre con. Mais j’ai vu ce qu’etais les exception maintenant. Pourquoi pop_back() ne jetterais pas une exception quand il opere sur une chaine ou un tableau vide? C’est pareil pour les acces en dehors du tableau. Pourquoi partout des UB? J’ai meme verifier j’avais du mal a y croire

Parce que ce sont des erreurs de programmation, et pas des erreurs contre lesquelles tu ne peux te prémunir. En gros, cela reste dans l’optique d’une programmation par contrat :

  • si tu respectes la précondition d’une opération ;
  • alors l’opération fera le job prévu.

Si ce n’est pas le cas rien n’est garanti. C’est ce qui permet notamment d’éviter un payer un surcoût (potentiellement important) à l’exécution. La manière de s’en prémunir est "simple", on doit garantir par construction de notre programme, que ces cas ne peuvent pas arriver. (Et il existe des techniques permettant de prouver mathématiquement que c’est le cas).

En comparaison, si tu prends une opération comme "j’essaie de lire une donnée sur le réseau", si la connexion tombe, tu ne peux t’en prémunir programmatiquement : il serait donc pertinent de traiter un tel cas par une exception.

III-19.2.2. Et avant, on faisait comment?

Un autre avantage, c’est de pouvoir réutiliser une même variable plusieurs fois, alors que la décomposition C++17 ne nous le permet pas.

Je ne comprends pas bien j’ai essaye d’utiliser une variable de strutured binding plusieurs fois ça a marché.

Et puis pour moi le comité est parfois lent un peu. On aurait pu facilement faire (moi ça me saute aux yeux):

[ nom, void, age ]  = std::tuple { "kill", "Bill", 24 };//ou
[ nom, _, age ]     = std::tuple { "kill", "Bill", 24 };//mais je prefere le premier

et puis comme ça on bute std::tie et std::ignore. Tranquille.

Il y a pas mal de considérations techniques dans le choix d’une syntaxe et d’une nouvelle fonctionnalité dans un langage. Dans un cas comme C++, où le parsing du langage est l’un des (le ?) plus complexe parmi les langages programmation utilisés en production. La question n’est pas si simple à résoudre.

Typiquement si je prends juste le bout :

[ a , b , c ] ...

Suis-je en train de préparer, un accès de tableau bien planqué :

[ a , b, c ] 1 = 3 ;

ou un structured binding ?

[ a, b, c ] = some_tuple ;

Et pour le cas du _. La norme nous dit que, comme cette chaîne commence par un _, c’est un identifiant réservé. MAIS, c’est aussi un identifiant valide. Deux possibilités pour que ça se passe mal dans du code existant.

Etc.

Il n’est pas si simple de faire évoluer un langage comme C++.

// On vérifie que a et b sont égaux.
bool operator==(Fractionconst& a, Fractionconst& b)
{
    Fractionconstgauche { a.numerateur* b.denominateur,a.denominateur* b.denominateur };
    Fractionconstdroite { b.numerateur* a.denominateur,b.denominateur* a.denominateur };
    return gauche.numerateur== droite.numerateur && gauche.denominateur== droite.denominateur;
}

On peut difficilement faire plus verbeux. La encore je me trompe peut-être. J’ai personne a cote de moi pour vérifier si je dis une connerie ou ne serait-ce qu’en discuter. Mieux(plus cour et plus lisible):

bool operator==(Fractionconst& a, Fractionconst& b)
{
    return { b.numerateur* a.denominateur ==  a.numerateur * b.denominateur };
}

Il faudrait peut être aussi expliqué pourquoi la solution naïve qui consisterait a évaluer l’égalité des division des deux membres est trop fragile. Je pense en particulier a la représentation des nombres en mémoire et aussi au nombre irrationnels. Et si je dis une bêtise une fois de plus, soyez attentif a me corriger.

mougnolribama

Solution à base de multiplication : si ton denominateur et ton numérateur sont suffisamment grands, tu risques de faire un integer overflow (qui est un UB dans la norme). Cela voudrait dire que :

  • soit tu dois fixer un contrat qui restreint la taille de tes fractions,
  • soit tu dois faire des conditions pour vérifier que tu ne dépasses pas mais ça augmenterait la verbosité.

Solution à base de division, on est sur des entiers, donc avec division entière. Ce qui veut dire que 1/2 == 1/3 == ... == 1/n (== 0), donc ça ne peut pas marcher comme ça. Alors on peut passer en float pour raisonner sauf que :

  • c’est cher de convertir,
  • il va y avoir des approximation (et donc de l’imprécision de conversion),
  • le calcul de division est l’un des plus longs sur un processeur.

A la limite, plus simplement :

bool operator==(Fractionconst& a, Fractionconst& b){
  return b.numerateur == a.numerateur && a.denominateur == b.denominateur;
}

C’est la même chose. Mais j’imagine que les étapes intermédiaires sont là à fin d’illustration.

D’abord Mes remerciements à l’auteur de ce cour qui en plus de m’abreuver de sa science me fait depuis un certains temps déjà l’honneur de faire partie de l’illustre groupe des relecteurs de la Bêta. Voila ça faisait longtemps que j’aurais dû le faire. Bref.

Ca doit forcement etre con. Mais j’ai vu ce qu’etais les exception maintenant. Pourquoi pop_back() ne jetterais pas une exception quand il opere sur une chaine ou un tableau vide? C’est pareil pour les acces en dehors du tableau. Pourquoi partout des UB? J’ai meme verifier j’avais du mal a y croire

Parce que ce sont des erreurs de programmation, et pas des erreurs contre lesquelles tu ne peux te prémunir. En gros, cela reste dans l’optique d’une programmation par contrat :

  • si tu respectes la précondition d’une opération ;alors l’opération fera le job prévu.
  • Si ce n’est pas le cas rien n’est garanti. C’est ce qui permet notamment d’éviter un payer un surcoût (potentiellement important) à l’exécution. La manière de s’en prémunir est "simple", on doit garantir par construction de notre programme, que ces cas ne peuvent pas arriver. (Et il existe des techniques permettant de prouver mathématiquement que c’est le cas).

En comparaison, si tu prends une opération comme "j’essaie de lire une donnée sur le réseau", si la connexion tombe, tu ne peux t’en prémunir programmatiquement : il serait donc pertinent de traiter un tel cas par une exception.

Ouais donc c’est normale en fait. Mais comme c’est géré autrement par tous les langages plus jeunes y compris ce qui cherchent à taper très haut dans les performances…

Et pour le cas du . La norme nous dit que, comme cette chaîne commence par un , c’est un identifiant réservé. MAIS, c’est aussi un identifiant valide. Deux possibilités pour que ça se passe mal dans du code existant.

OK c’est vraiment bizarre comme identifiant. Merci. _ c’est une lettre. Mince!

Typiquement si je prends juste le bout :

[ a , b , c ] …

Suis-je en train de préparer, un accès de tableau bien planqué :

[ a , b, c ] 1 = 3 ;

ou un structured binding ?

[ a, b, c ] = some_tuple ;

L’accès au tableau j’ai pas trop (pas du tout)compris. Tu peux détailler s’il te plaît?

Les integer overflow c’est carrément un morceau(toujours c’est bêtes U.B un peu partout). Il y a plein de chose a dire et a comprendre et je ne suis pas forcement chaud pour me lancer la dedans en profondeur maintenant(Je suis un cour sur le c++ par sur le c; j’aimerais pouvoir me passer de comprendre autant que possible toutes les horreurs qui se passe dans le sous-système c du langage c++). J’aimerais cependant éclaircir un point.

ici

// On vérifie que a et b sont égaux.
bool operator==(Fractionconst& a, Fractionconst& b)
{
    Fractionconstgauche { a.numerateur* b.denominateur,a.denominateur* b.denominateur };
    Fractionconstdroite { b.numerateur* a.denominateur,b.denominateur* a.denominateur };
    return gauche.numerateur== droite.numerateur && gauche.denominateur== droite.denominateur;
}

comme là

bool operator==(Fractionconst& a, Fractionconst& b){
  return b.numerateur == a.numerateur && a.denominateur == b.denominateur;
}

on peut avoir dépassement entier non(dans les deux cas on multiplie 2 int possiblement très grand qu’on stocke dans un int qui va peut être exploser)? Si non la je pense que c’est pour maintenant et pas pour demain ma véritable rencontre avec les types primitifs du c(qui sont peut être finalement évolués de par la complexité des problème qu’il soulève).

Solution à base de division, on est sur des entiers, donc avec division entière. Ce qui veut dire que 1/2 == 1/3 == … == 1/n (== 0), donc ça ne peut pas marcher comme ça. Alors on peut passer en float pour raisonner sauf que :

  • c’est cher de convertir,
  • il va y avoir des approximation (et donc de l’imprécision de conversion),
  • le calcul de division est l’un des plus longs sur un processeur.

c’est cher: c’est concernant les performances? le calcul de la division: encore des performances? il va y avoir des approximation: c’est la seul raison qui me rebute réellement.

Après j’ai vu passer des annonces avec des types infini en c++(??). Ça ne doit pas être trivial comme notion je pense (en plus que ça doit pas déjà être implémenté; du moins sans bug).

Édité par mougnolribama

in medio stat virtus

+0 -0

D’abord Mes remerciements à l’auteur de ce cour qui en plus de m’abreuver de sa science me fait > Ouais donc c’est normale en fait. Mais comme c’est géré autrement par tous les langages plus jeunes y compris ce qui cherchent à taper très haut dans les performances…

Oui, et non. Dans des langages, il y a tout un tas de constructions qui permettent d’avoir certaines garanties (notamment par typage), qui permettent de supprimer les contrôles de bornes et de ne pas trop perdre de performances. L’exemple typique, c’est quelque chose comme :

std::vector<int> v ;
// du code
for(std::size_t i = 0 ; i < v.size() ; i++){
  v[i] = 42 ; // par construction cette opération ne déborde pas
}

Ici, on n’a aucun intérêt à ajouter un contrôle lors de la phase de compilation, mais ce n’est pas si facile à généraliser. Les langages modernes exploitent le fait que l’on a tout un tas de constructions de ce genre pour n’ajouter les contrôles qu’au cas par cas et éviter un surcoût trop important (mais pas tout le surcoût).

Quant à "pourquoi ne pas ajouter ça à C++ ?" : parce que ça pourrait potentiellement faire s’effondrer les performances de code qui est actuellement en production, et que ça freinerait énormément l’adoption d’une norme qui ferait ça.

Suis-je en train de préparer, un accès de tableau bien planqué :

[ a , b, c ] 1 = 3 ;

L’accès au tableau j’ai pas trop (pas du tout)compris. Tu peux détailler s’il te plaît?

Je me suis emmêlé les pinceaux avec les syntaxes ésotériques de C++, en fait celle-ci ne serait pas acceptée (c’est un truc comme 1[a] qui serait acceptable et donc par extension 1[a, b, c] puisque l’on a l’opérateur , qui permet ce genre de joyeuseté).

En revanche pour revenir sur les justifications de pourquoi ce serait pénible pour le structured binding, l’avantage de commencer la déclaration par un type est que c’est très simple à repérer et que ça évite d’avoir plusieurs manière différentes de rentrer dans le mode "déclaration". De plus, ça évite de créer des erreurs trop cryptiques.

Par exemple, dans l’expression :

a[i] = 42 ;

Il est facile de supprimer le a par mégarde et dans ce cas, on aurait potentiellement une erreur :

Redeclaration of `i`

Alors qu’en fait, on voudrait avoir une syntax-error, bête et méchante.

Les integer overflow c’est carrément un morceau(toujours c’est bêtes U.B un peu partout). Il y a plein de chose a dire et a comprendre et je ne suis pas forcement chaud pour me lancer la dedans en profondeur maintenant(Je suis un cour sur le c++ par sur le c; j’aimerais pouvoir me passer de comprendre autant que possible toutes les horreurs qui se passe dans le sous-système c du langage c++).

Pour le coup, ces UBs font en grande partie de la raison pour laquelle C et C++ peuvent être compilés vers du code efficace sur toutes les architectures. Typiquement, c’est précisément un défaut que le débordement sur les non-signés soit spécifié, s’il avait été UB, ça aurait simplifié la vie à tout le monde.

Il y a rien à comprendre à propos des UBs sur les entiers en C et C++ : on doit s’arranger pour qu’il n’y en ait pas dans le code. Et c’est tout.

On a besoin des UBs pour :

  • produire du code efficace dans certaines situations
  • permettre l’adaptation du langage à différentes architecture

Aucun langage hautes performances n’y fait exception. Même pas Rust, qui a introduit les blocs unsafe précisément pour ça.

J’aimerais cependant éclaircir un point.

ici

// On vérifie que a et b sont égaux.
bool operator==(Fractionconst& a, Fractionconst& b)
{
    Fractionconstgauche { a.numerateur* b.denominateur,a.denominateur* b.denominateur };
    Fractionconstdroite { b.numerateur* a.denominateur,b.denominateur* a.denominateur };
    return gauche.numerateur== droite.numerateur && gauche.denominateur== droite.denominateur;
}

comme là

bool operator==(Fractionconst& a, Fractionconst& b){
  return b.numerateur == a.numerateur && a.denominateur == b.denominateur;
}

on peut avoir dépassement entier non(dans les deux cas on multiplie 2 int possiblement très grand qu’on stocke dans un int qui va peut être exploser)? Si non la je pense que c’est pour maintenant et pas pour demain ma véritable rencontre avec les types primitifs du c(qui sont peut être finalement évolués de par la complexité des problème qu’il soulève).

Un point que j’aurais dû préciser dans ma réponse est que je considère comme invariant que la fraction est toujours dans un état irréductible (qui est unique pour chaque fraction). Donc pour celui que je propose, il n’y a pas de multiplication (et donc pas de risque d’overflow).

Pour le premier code, oui, il y a aussi un risque de dépassement, qu’il faut prendre en compte lors de la définition des invariants et conditions d’usage de la classe Fraction.

c’est cher: c’est concernant les performances? le calcul de la division: encore des performances? il va y avoir des approximation: c’est la seul raison qui me rebute réellement.

Pour la conversion, de ce que je lis, 6 à 15 cycles pour transformer d’entier à double. Pour la division en double, ça peut taper dans les 25 cycles. Une comparaison d’entier c’est 1 cycle.

Et pou les approximations, oui à mort. Sans compter le fait que le calcul à virgule flottante est démesurément plus complexe que le calcul entier.

Après j’ai vu passer des annonces avec des types infini en c++(??). Ça ne doit pas être trivial comme notion je pense (en plus que ça doit pas déjà être implémenté; du moins sans bug).

mougnolribama

Tout ce qui est typé à taille arbitraire, ça existe depuis longtemps et il y a déjà de super implémentations. Mais les coûts sont très rapidement prohibitifs (des coûts tels qu’on peut douter de l’utilité de manipuler ça directement en C++).

PUB: Si les questions de code correct dans ce genre de langages t’intéressent, je te conseille de jeter un oeil à mon tutoriel sur la preuve de programmes C (dans ma signature).

Édité par Ksass`Peuk

Salut à tous.

Je reviens après un mois d’inactivité pour vous dire que les chapitres sur la sémantique de mouvement et sur les handles sont terminés, n’hésitez donc pas à les lire. Je saute pour l’instant le TP et je continue le chapitre qui approfondit l’héritage, avec le NVI notamment.

Merci d’avance à tous pour votre 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.

Édité par informaticienzero

Je remarque sur le discord de NaN qu’il y a régulièrement des débutants qui tombent sur des erreurs qu’un compilateur détecte facilement, mais qu’il n’est pas configuré pour (notamment gcc et msvc).

Se serait bien d’ajouter dans « Le minimum pour commencer » un petit mots sur les avertissements essentiels (-Wall -Wextra, eventuellement -pedantic pour gcc/clang et \W3(?) pour msvc).

+1 -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