La programmation en C++ moderne

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

a marqué ce sujet comme résolu.

J’ai mis à jour le tutoriel en intégrant les remarques de lmghs, mais j’ai également apporté plusieurs corrections orthographiques, quelques détails supplémentaires ainsi que le début du chapitre sur les tableaux. Dans celui-ci, je parlerai de std::vector et std::array, des boucle for pour conteneurs ainsi que de comment lire la documentation. Et je réfléchis également à la gestion des erreurs.

Merci d’avance pour vos commentaires.

Corrections diverses un peu partout, comme par exemple sur la position de const. Et surtout ajout du chapitre sur les tableaux, avec déjà la partie sur std::vector. Je me sers de ce chapitre également pour introduire la documentation et accompagner les lecteurs dans son exploration (encore basique).

Merci d’avance pour vos commentaires.

Parenthèse au sujet du placement de const. Il y a eu un petit débat sur reddit/r/cpp il y 2-3 semaines suite à un post d’un auteur préférant le east const. La communauté est majoritairement west const (même si on pourrait critiquer son incohérence vu qu’elle devrait être const west), pire les CppCoreGuideline présentent un avis tranché sur la question.

Ma tendance est à être east const sur les paramètres et les déclarations de références constantes. En revanche, sur les constantes, je tends à être à l’ouest. Ceci dit, on a constexpr pour quelques cas maintenant.

Bref, tout ça pour dire, que ce n’est pas grave tant que tu es cohérent.


Tableaux

vecteur

Mais maintenant, j’ai besoin de les stocker.

Pour la moyenne, en fait non. Un istream_itérator est un input iterator, et ça suffit pour accumuler. Pour un calcul de variance, là oui, en revanche: il faut stocker vu que deux passes sont nécessaires.

Maintenant, collégiens et lycéens ne vont pas forcément connaître le terme. :(

Il en existe même pour stocker des fonctions !

Je n’en aurai pas parlé. J’aurai plus parlé de paires d’éléments et autres agrégats de données comme des coordonnées de points.

des élément

s/$/s/

Commençons avec les tableaux dynamiques, c’est à dire qui peuvent stocker un nombre variable d’éléments et dont la taille peut augmenter ou diminuer à volonté.

La définition originelle qui est partagée par beaucoup langages: un tableau dynamique est un tableau dont la taille ne sera connue qu’à l’exécution. Exemple typique: on lit des notes dans un fichier, on ne sait pas combien il y a de lignes dans le fichier au moment où l’on écrit et compile notre programme. Il faut pouvoir s’adapter dynamiquement.

Le vecteur a une seconde particularité: il est facilement redimensionnable. C’est un double dynamisme en quelques sorte.

Et le fichier correspondant

-> "fichier d’en-tête" correspondant qui permet de l’utiliser[…]"

Voici le lien vers la documentation anglophone

Peut-être une notre de bas de page comme quoi la doc francophone est majoritairement une traduction automatique.

Examinons-la ensemble.

Peut-être un peu prématuré. Montre des exemples avant. Le plus typique: tu remplis et tu parcours pour un calcul de moyenne ou autre.

vector<> const

Pour un vecteur vide, c’est peu bizarre.

std::vector</* type du tableau */> identifiant {};

-> type des éléments du tableau

on peut très bien leur assigner des valeurs

Je n’aime pas ce mot. Il sonne très anglicisme.

[UB]

Dans mes formations je place des phrases comme, "l’UB, c’est parce que vous avez commis une erreur de programmation, il ne fallait pas faire la fête hier soir, le compilateur est en droit de transformer votre code en programme qui envoie un chat en orbite."

int VS size_t

Il y a de gros débats chez les experts en ce moment. Il s’en dégage que size_t est une erreur qui a été faite et que l’on se traine. Même si c’est sémantiquement juste, cela implique des problèmes:

  • de sécurité quand nos algos nous font décrémenter
  • de performances car UINT_MAX + 1 est défini alors INT_MAX + 1 est une UB. Cela veut dire que le compilateur doit réaliser des vérifications à la compilation ou au pire à l’exécution, cela implique qu’une incrémentation de size_t coûte régulièrement plus cher qu’une incrémentation de ptr_diff_t
  • de pénibilité: for (auto i=0 ; i!=size(v) ; ++i) => warning

Ce type c’est std::size_t. Et c’est ce type que nous allons utiliser.

C’est une super occasion pour sortir auto, façon de dire: "dans le doute, avec auto votre variable sera du bon type"

for (/* type d'un élément du tableau */ identifiant : /* tableau à parcourir */)

Sa syntaxe est étrange. certains bouts sont des commentaires, d’autres écrits sans mise en forme (identifiant). Cette convention d’écriture peut être très surprenante.

Pour l’exemple qui suit, tu utilises endl alors que tu utilises \n sur le précédent. Aujourd’hui je suis passé dans le camp des \n.

void push_back(T&& value);

Il y en a deux de versions de push_back, et pour être juste, ton exemple qui suit utilise l’autre.

Cela signifie que nous allons ajouter(*)

(*) à la fin

for (int const i : tableau_de_int)

En termes de bonnes pratiques,quand on autorise un entier à s’appeler i ou j, c’est parce qu’il dénote un indice, souvent employé pour parcourir un tableau. Pour référencer une élément de tableau, ce n’est pas idéal.

for (std::string const chaine : tableau_de_string)

Une référence constante est préférable ici.

retrait d’élément

Il y a aussi erase & cie

int VS size_t

Il y a de gros débats chez les experts en ce moment. Il s’en dégage que size_t est une erreur qui a été faite et que l’on se traine. Même si c’est sémantiquement juste, cela implique des problèmes:

  • (1) de sécurité quand nos algos nous font décrémenter
  • (2) de performances car UINT_MAX + 1 est défini alors INT_MAX + 1 est une UB. Cela veut dire que le compilateur doit réaliser des vérifications à la compilation ou au pire à l’exécution, cela implique qu’une incrémentation de size_t coûte régulièrement plus cher qu’une incrémentation de ptr_diff_t
  • (3) de pénibilité: for (auto i=0 ; i!=size(v) ; ++i) => warning
lmghs

(1) Premier argument pertinent que je vois à ce sujet. Même si je le classerai comme argument de pénibilité d’écriture. La sécurité, elle est censée être réalisée à l’aide d’outils qui ne louperaient pas ça.

(2) Deux solutions qui ne seront jamais proposées parce que rétro-compatibilité, ce serait un type qui wrap et l’autre non, ou l’utilisation d’un attribut pour préciser au compilateur si on veut qu’un non signé wrap.

(3) Toujours pas d’accord, vu la verbosité crasse de ce langage et les horreurs de syntaxe qu’on continue d’ajouter à la norme, on pas chialer pour rajouter ul ou ull selon ce dont on a réellement besoin.

Merci à tous les deux pour vos retours. Pour ce qui concerne les fautes de français je ne les relève pas pour ne pas alourdir ce message, mais je les corrige.

Il en existe même pour stocker des fonctions !

Je n’en aurai pas parlé. J’aurai plus parlé de paires d’éléments et autres agrégats de données comme des coordonnées de points.

lmghs

Quand je pensais aux objets pour stocker des fonctions, je pensais en fait aux classes, mais ça n’était pas évident je te l’accorde.

Examinons-la ensemble.

Peut-être un peu prématuré. Montre des exemples avant. Le plus typique: tu remplis et tu parcours pour un calcul de moyenne ou autre.

lmghs

Je sais que c’est complexe. Mais je cherchais une façon d’introduire l’exploration de la documentation, et je me disais que ce chapitre était bien adapté.

Il y a de gros débats chez les experts en ce moment. Il s’en dégage que size_t est une erreur qui a été faite et que l’on se traine. Même si c’est sémantiquement juste, cela implique des problèmes:

  • de sécurité quand nos algos nous font décrémenter
  • de performances car UINT_MAX + 1 est défini alors INT_MAX + 1 est une UB. Cela veut dire que le compilateur doit réaliser des vérifications à la compilation ou au pire à l’exécution, cela implique qu’une incrémentation de size_t coûte régulièrement plus cher qu’une incrémentation de ptr_diff_t
  • de pénibilité: for (auto i=0 ; i!=size(v) ; ++i) => warning
lmghs

Du coup tu me conseille de présenter plutôt unsigned int ?

Ce type c’est std::size_t. Et c’est ce type que nous allons utiliser.

C’est une super occasion pour sortir auto, façon de dire: "dans le doute, avec auto votre variable sera du bon type"

lmghs

Pas bête du tout, je vais voir comment formuler ça.

for (/* type d'un élément du tableau */ identifiant : /* tableau à parcourir */)

Sa syntaxe est étrange. certains bouts sont des commentaires, d’autres écrits sans mise en forme (identifiant). Cette convention d’écriture peut être très surprenante.

Je me suis dit que si je mettais identifiant en commentaire, certain penseraient que le : est une erreur de frappe dans le code.

Pour l’exemple qui suit, tu utilises endl alors que tu utilises \n sur le précédent. Aujourd’hui je suis passé dans le camp des \n.

lmghs

Je me rappelle des arguments que tu avais sorti en faveur de '\n'. Je mets les deux régulièrement pour habituer le lecteur aux deux.

for (int const i : tableau_de_int)

En termes de bonnes pratiques,quand on autorise un entier à s’appeler i ou j, c’est parce qu’il dénote un indice, souvent employé pour parcourir un tableau. Pour référencer une élément de tableau, ce n’est pas idéal.

lmghs

Effectivement, je vais changer ça.

for (std::string const chaine : tableau_de_string)

Une référence constante est préférable ici.

lmghs

Je n’ai pas encore introduit le concept des références à ce stade. Je compte les aborder dans le chapitre suivant. "Vous voyez quand on passe des string ou des vector à nos fonctions, ils sont recopiés. Pareil pour nos boucles. Mais hop, super concept qui vient à notre secours : les références."

int VS size_t

Il y a de gros débats chez les experts en ce moment. Il s’en dégage que size_t est une erreur qui a été faite et que l’on se traine. Même si c’est sémantiquement juste, cela implique des problèmes:

  • (1) de sécurité quand nos algos nous font décrémenter
  • (2) de performances car UINT_MAX + 1 est défini alors INT_MAX + 1 est une UB. Cela veut dire que le compilateur doit réaliser des vérifications à la compilation ou au pire à l’exécution, cela implique qu’une incrémentation de size_t coûte régulièrement plus cher qu’une incrémentation de ptr_diff_t
  • (3) de pénibilité: for (auto i=0 ; i!=size(v) ; ++i) => warning
lmghs

(1) Premier argument pertinent que je vois à ce sujet. Même si je le classerai comme argument de pénibilité d’écriture. La sécurité, elle est censée être réalisée à l’aide d’outils qui ne louperaient pas ça.

(2) Deux solutions qui ne seront jamais proposées parce que rétro-compatibilité, ce serait un type qui wrap et l’autre non, ou l’utilisation d’un attribut pour préciser au compilateur si on veut qu’un non signé wrap.

(3) Toujours pas d’accord, vu la verbosité crasse de ce langage et les horreurs de syntaxe qu’on continue d’ajouter à la norme, on pas chialer pour rajouter ul ou ull selon ce dont on a réellement besoin.

Ksass`Peuk

(3) je suis d’accord avec toi, je tâchais d’être exhaustif.

(2) C’est un problème que j’ai aujourd’hui étant passé sur un contexte HPC, et parfois, sur certains compilos en fonction du contexte, la différence se mesure vraiment. Il me semblait que la STL v2 des ranges passait aux signés.

(1) Effectivement, c’est le job des outils. J’ai vu passer des propositions dans le dernier mailling à propos des mélanges signed/unsigned, mais je n’ai pas creusé ce que cela pourrait apporter au débat.

Du coup tu me conseille de présenter plutôt unsigned int ?

Non surtout pas. Le sens de mes remarques est qu’il m’est difficile de dire que size_t est le type évident car il est non-signé. Du coup, attention à la formulation.

Je publie donc pour que vous puissiez lire la version modifiée / corrigée suite à certaines des remarques précédentes plus le début de la partie sur les algorithmes. Je survole pour l’instant les itérateurs pour ne parler que de std::begin et std::end. Je pense les introduire plus tard dans le cours. Là, ça rendrait le chapitre bien trop lourd et un débutant n’en verrait pas vraiment l’utilité.

Merci d’avance pour vos commentaires.

Je ne sais pas si c’est choix volontaire ou non, il n’y a pas toujours besoin de préfixer begin() par l’espace de noms. Cf plus de détails ma prose sur un sujet connexe qui a tardé à sortir: https://linuxfr.org/news/cpp17-libere-size-data-et-empty#et-pour-sen-servir

Pour un débutant, je ne sais pas ce qui est le plus compliqué: donner begin(vect) (qui sous-entend plein de choses, mais qui est plus simple à écrire), ou std::begin(vect) (qui est régulier, mais plus lourd à écrire), sachant qu’aucun des deux n’est parfait.

for (auto i {0}; i < taille; ++i)

Et là, il y a un piège: i est ici un int, tout ce qu’il y a de plus signé. Il faudrait utiliser explicitement size_t, ou 0ULL, ou auto i = decltype(taille){0} (pas sûr que la syntaxe soit valide, je ne me souviens jamais) pour un choix de type 100% exact.

void pop_back();

ne lance aucune exception, mais elle n’est pas marquée noexcept? Que c’est étrange.

total / notes.size()

Attention aux arrondis. On pourrait aussi pinailler si le flux d’entrée est fermé

Je préfère la version avec std:: explicitement, simple préférence personnelle. Ceci dit, ça m’évite d’expliquer les namespaces tout de suite.

Pour pop_back, ni cplusplus ni cppreference ne sont correct. Mais je vais le rajouter.

Pour la boucle, effectivement, je m’en suis rendu compte après avoir écrit mais je n’ai pas encore corrigé parce que je n’avais pas accès à un ordinateur. Je vais réfléchir peut-être à parler de unsigned dans le chapitre sur les variables et ensuite rajouter 0ULL avec auto.

Au sujet de la PpC, mon expérience de son enseignement en formation me fait dire que c’est un sujet avancé. Ce n’est pas tant qu’il soit complexe, c’est une question qu’un débutant (même développeur de formation) va avoir plus de mal à l’appréhender que quelqu’un de plus expérimenté (qui n’est pas forcément un développeur à la base).

Comme tu le vois avec la longueur de mes billets de blogs, il y a plusieurs choses à présenter. Oui, assert sert à affirmer dans quel état nous sommes censés être et que sinon c’est une erreur de programmation. Que l’outil sert à faire du failfast pour avoir un contexte précis et utile et non décalé (car avec les UBs, on ne sait jamais quand ça va planter) – cela peut nécessiter de faire une démo avec un débugueur. Il y a un autre aspect difficile, d’autant plus les pour les moins expérimentés: qui est responsable de faire les vérifications des entrées (du programme et non des fonctions, la confusion est facile) et d’assurer que nous sommes dans une situation valide.

Bref, je n’en parlerai pas là. Dire qu’il y a un contrat sur vector::operator[](), et que c’est à l’utilisateur de savoir ce qu’il fait est une chose. Rentrer plus en détails sur les assertions à ce niveau? J’ai peur que cela soit prématuré.

Je sais que gbdivers en parle dans son cours. Après est-ce que le niveau visé est exactement le même, c’est une bonne question.

On est d’accord, la PpC est complexe et je n’en saisi pas tous les tenants et les aboutissants moi-même. Après, je pense que c’est une bonne chose d’introduire quelque chose, d’utiliser basiquement les assertions (à l’exécution et plus tard dans le cours statiques). Le lecteur ne deviendra pas un professionnel de la PpC, il aura encore beaucoup à apprendre, tout comme il aura beaucoup à apprendre sur le C++.

Je n’ai pas vu dans ton cours où tu abordes la PpC (mais j’ai vu la partie sur les asserts).

Dans mon cours, je parle (dans la dernière version) des asserts dès le début. La raison est que je trouve que tester un programme en le lançant pour le tester manuellement est une très très mauvaise pratique. Donc je parle des asserts au début, puis je parle des TU lorsque j’aborde les fonctions.

Et je trouve la PpC intéressante pour expliquer comment on conçoit les fonctions. Ca suit une logique "qu’est ce que je veux faire" puis "quel est la syntaxe pour le faire".

+3 -0

Malgré tout, le contrat pour une fonction et les assertions elles-mêmes, induisent dès le début une réflexion pour le débutant : comprendre pourquoi son programme fonctionne. Et c’est à mon avis le problème qu’ont beaucoup de cours, on cherche à faire coder sans comprendre ce qui fait que ça marche. Et assurer que "ça marche", ça implique déjà de comprendre "ce que je veux", et c’est le moment où les assertions sont là : faire dire au débutant ce qu’il veut de son programme.

C’est con, mais on ne l’exprime pas suffisamment. On se concentre beaucoup trop sur le comment faire avant même d’avoir compris quoi et pourquoi faire.

J’ai terminé le chapitre sur les tableaux et j’ai commencé celui sur les fonctions. Celui-ci introduira la création des fonctions, la lecture de la documentation, les références, la gestion des erreurs. Je réfléchis également à introduire les polymorphismes ah-doc et générique, mais j’ai peur que ça fasse trop. Les lambdas seront introduites en même temps que les algorithmes et les itérateurs, dans le chapitre suivant.

Merci d’avance pour vos commentaires.

Tu es motivé en ce moment :D

Je n’ai jamais pris le temps de lire ton cours et les codes d’exemple, du coup j’ai quelques questions.

  • tu utilises parfois \n, parfois std::endl. Il faudrait homogeneiser ? (Ou c’est volontaire ?)

  • dans ce code, tu utilises auto. Ca serait pas mieux d’utiliser decltype(compteur) ?

1
for (auto i { 0u }; i < compteur; ++i)
  • tu utilises des unsigned, mais il y a eu plusieurs discussions (a CppCon et sur les forums ici je crois) a propos de l’utilisation de signed vs unsigned. Et l’idée generale etait d’utiliser signed par defaut et reserver unsigned pour représenter les tailles des collections (héritage historique de la STL). Tu en penses quoi pour dans un cours ?

  • pas de déduction de type dans ce code ? (Alors que tu utilises la deduction avec {} dans la boucle for. A homogénéiser ?)

1
unsigned int repetitions { 3u };
+2 -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