Parcours
/
C++
C++
/
Exercices
/
maître des lasagnes
maître des lasagnes

maître des lasagnes

Exercice d'apprentissage

Introduction

Terminologie

Tu as déjà utilisé et écrit des fonctions C++ dans quelques concepts. Il est temps de passer à la technique. L'extrait de code ci-dessous présente les termes les plus courants, pour référence. Comme le C++ ignore les espaces blancs, la mise en forme a été modifiée pour placer chaque élément sur une seule ligne.

// Function declaration:
bool                                              // Return type
admin_detected(string user, string password)      // Type signature
;                                                 // Don't forget the ';' for the declaration

// Function definition:
bool                                              // Return type
admin_detected                                    // Function name
(string user, string password)                    // Parameter list
{ return user == "admin" && password == "1234"; } // Function body
Advanced

La déclaration fonctionne comme une note adressée au compilateur : elle indique qu'il existe une fonction avec ce nom, ce type de retour et cette liste de paramètres. Le code ne fonctionnera pas si la définition est absente. Les déclarations sont facultatives : elles sont nécessaires si tu utilises la fonction avant sa définition. Les déclarations peuvent résoudre des problèmes comme les références cycliques, et elles permettent de séparer l'interface de l'implémentation.

Le qualificatif const

Parfois, on veut s'assurer que des valeurs ne peuvent pas être modifiées après avoir été initialisées. Le C++ utilise le mot-clé const comme qualificatif pour les constantes.

const int number_of_dragon_balls{7};
number_of_dragon_balls--; // compilation error
Note

Tu verras souvent des constantes écrites en UPPER_SNAKE_CASE. Il est recommandé de réserver cette casse aux macros, s'il n'existe pas d'autre convention.

Si tu essaies de modifier une variable constante après sa définition, ton code ne compilera pas. Cela permet d'éviter des modifications involontaires, mais ouvre aussi des possibilités d'optimisation pour le compilateur. En tant qu'humain, il est aussi plus facile de raisonner sur le code quand on sait que certaines parties ne seront pas affectées.

On peut aussi utiliser const comme qualificatif pour les paramètres d'une fonction.

string guess_number(const int& secret, const int& guess) {
    if (secret < guess) return "lower.";
    if (secret > guess) return "higher.";
    return "exact!";
}

Quand tu passes une référence const à la fonction, tu peux être sûr qu'elle restera inchangée. Tu verras souvent des références const pour des objets dont la copie peut être coûteuse, comme des strings plus longs. Un troisième cas d'usage du qualificatif const concerne les fonctions membres qui ne modifient pas l'instance d'une classe.

class Stubborn {
    public:
    Stubborn(string reply) {
        response = reply;
    }
    string answer(const string& question) const {
        if (question.length() == 0) { return ""; }
        return response;
    }
    private:
    string response{};
};

La fonction membre answer de Stubborn utilise une référence const string& comme paramètre. Cela évite une opération de copie depuis l'objet d'origine passé à la fonction.

La surcharge de fonctions

Plusieurs fonctions peuvent porter le même nom si leur liste de paramètres est différente. C'est ce qu'on appelle la surcharge de fonctions, et on y a généralement recours quand ces fonctions effectuent des tâches très similaires.

L'en-tête de la fonction, sans le type de retour, constitue sa signature de type. Un changement dans la signature de type donne une nouvelle fonction.

L'exemple play_sound a six surcharges différentes pour répondre à différents scénarios :

// different argument types:
void play_sound(char note);         // C, D, E, ..., B
void play_sound(string solfege);    // do, re, mi, ..., ti
void play_sound(int jianpu);        // 1, 2, 3, ..., 7

// different number of arguments:
void play_sound(string solfege, double duration);

// different qualifiers:
void play_sound(vector<string>& solfege);
void play_sound(const vector<string>& solfege);
Advanced

La signature de type est définie par le nom de la fonction, le nombre de paramètres, leurs types et leurs qualificatifs (mais pas leurs noms). Le type de retour ne fait explicitement pas partie de la signature de type, et tu obtiendras des erreurs de compilation si deux fonctions ne diffèrent que par leur type de retour. Le compilateur se plaindra, car il n'est pas clair laquelle des deux doit être utilisée.

Les arguments par défaut

Certaines fonctions peuvent devenir très longues, et beaucoup de leurs appels utilisent souvent les mêmes valeurs pour la plupart des paramètres. On peut éviter cette répétition dans les appels grâce aux arguments par défaut.

void record_new_horse_birth(string name, int weight, string color="brown-ish", string dam="Alruccaba", string sire="Poseidon");

record_new_horse_birth("Urban Sea", 130); // color will be brown, dam "Alruccaba", sire "Poseidon"
record_new_horse_birth("Highclere", 175, "off-white", "Fall Aspen");   // sire will be "Poseidon"

Comme la déclaration de la fonction est souvent lue avant sa définition, c'est l'endroit le plus approprié pour définir les arguments par défaut. Si un paramètre a une déclaration par défaut, tous les paramètres situés à sa droite doivent aussi avoir une déclaration par défaut. Parfois, des surcharges de fonctions compliquées peuvent être réécrites en moins de fonctions avec des arguments par défaut, afin d'améliorer la maintenabilité.

Instructions

Dans cet exercice, tu vas écrire un peu plus de code pour préparer et cuisiner ta géniale lasagne tirée de ton livre de cuisine préféré.

Tu as cinq tâches. La première concerne la cuisson elle-même, les quatre autres portent sur la préparation parfaite.

1. Estime le temps de préparation

Pour la prochaine lasagne que tu vas préparer, tu veux t'assurer de réserver assez de temps pour profiter de la cuisine. Tu as déjà prévu les couches de ta lasagne. Maintenant, tu veux estimer la durée de la préparation à partir de celles-ci.

Implémente une fonction preparationTime qui prend un vecteur de couches sous forme de string ainsi que le temps de préparation moyen par couche, en minutes, sous forme d'int. La fonction doit renvoyer l'estimation du temps total de préparation à partir du nombre de couches, sous forme d'int. Si le temps de préparation moyen n'est pas passé, la valeur par défaut de 2 doit être utilisée.

std::vector<std::string> layers{"sauce", "noodles", "sauce", "meat", "mozzarella", "noodles"};
preparationTime(layers, 3)
// => 18
preparationTime(layers)
// => 12

2. Calcule les quantités de nouilles et de sauce nécessaires

En plus de réserver le temps, tu veux aussi t'assurer d'avoir assez de sauce et de nouilles pour cuisiner la lasagne de tes rêves. Pour chaque couche de nouilles de ta lasagne, il te faut 50 grammes de nouilles. Pour chaque couche de sauce de ta lasagne, il te faut 0,2 litre de sauce.

Définis la fonction quantities qui prend un vecteur de couches comme paramètre, sous forme de string. La fonction détermine ensuite la quantité de nouilles et de sauce nécessaires pour préparer ton repas. Le résultat doit être renvoyé sous la forme d'une struct amount, qui est déjà définie pour toi. amount prend deux variables membres : la première pour le poids des noodles sous forme d'int, et la seconde pour le volume de la sauce sous forme de double.

struct amount { int noodles; double sauce; };

amount needed{quantities({"sauce", "noodles", "sauce", "meat", "mozzarella", "noodles"})};
// needed.noodles => 100
// needed.sauce => 0.4

3. Ajoute l'ingrédient secret

Il y a quelque temps, tu as rendu visite à un ami et tu as mangé de la lasagne chez lui. Elle était incroyable et avait un petit quelque chose de spécial. Ton ami t'a envoyé la liste des ingrédients et t'a dit que le dernier élément de la liste est l'« ingrédient secret » qui rendait ce repas si spécial. Maintenant, tu veux aussi ajouter cet ingrédient secret à ta recette.

Écris une fonction addSecretIngredient qui prend deux vector of strings en paramètres. Le premier paramètre est une référence aux ingrédients de ta propre recette, et le second vecteur de strings contient les ingrédients que ton ami t'a envoyés. Le dernier élément de ta liste d'ingrédients est toujours "?". La fonction doit le remplacer par le dernier élément de la liste de ton ami. Remarque : addSecretIngredient ne renvoie rien : tu dois modifier directement la liste de tes ingrédients. La liste des ingrédients de ton ami peut aussi être une référence (pour améliorer les performances), mais elle ne doit pas être modifiée. Assure-toi que la liste ne sera pas modifiée, à l'aide d'une référence const.

std::vector<std::string> friendsList{"noodles", "sauce", "mozzarella", "kampot pepper"};
std::vector<std::string> myList{"noodles", "meat", "sauce", "mozzarella", "?"};

addSecretIngredient(myList, friendsList);
// myList => {"noodles", "meat", "sauce", "mozzarella", "kampot pepper"}

4. Adapte la recette

Les quantités indiquées dans ton livre de cuisine ne donnent assez de lasagne que pour deux portions. Comme tu veux cuisiner pour plus de personnes la prochaine fois, tu veux calculer les quantités pour différents nombres de portions.

Implémente une fonction scaleRecipe qui prend deux paramètres.

  • Un vecteur de quantités double nécessaires pour 2 portions.
  • Le nombre de portions que tu veux cuisiner.

La fonction doit renvoyer un nouveau vecteur des quantités nécessaires pour le nombre de portions souhaité. Tu veux cependant conserver la recette d'origine. Cela signifie que l'argument quantities ne doit pas être modifié dans cette fonction.

std::vector<double> quantities{1.2, 3.6, 10.5};
scaleRecipe(quantities, 4);
// => { 2.4, 7.2, 21 }

5. Déverrouille le secret de famille

Tu te souviens d'histoires de ton enfance, des récits sur ton arrière-grand-tante, la gardienne des secrets culinaires les plus profonds de la famille. La légende raconte qu'elle détient la clé de saveurs au-delà de toute imagination, mais ses méthodes sont peu conventionnelles. Elle ne travaille pas avec des listes ni des mesures ; elle parle à voix basse et ne révèle qu'un seul ingrédient à la fois.

Mêlant excitation et appréhension, tu décides de contacter ton arrière-grand-tante. Comme prévu, elle répond de sa manière énigmatique et t'offre un unique ingrédient secret, enveloppé de mystère.

La contribution de ton arrière-grand-tante ajoute une couche de complexité que tu veux exploiter dans ta recette. Implémente une autre version de la fonction addSecretIngredient qui prend deux arguments : une référence vers un vecteur représentant ta liste d'ingrédients, et une string représentant l'offrande énigmatique de ton arrière-grand-tante.

std::vector<std::string> myList{"noodles", "meat", "sauce", "mozzarella", "?"};
std::string secretIngredient{"spice melange"};
addSecretIngredient(myList, secretIngredient);
// myList => {"noodles", "meat", "sauce", "mozzarella", "spice melange"}

Bonne chance, courageux chef !

Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet
C++ Exercism

Prêt à commencer maître des lasagnes ?

Inscris-toi sur Exercism pour apprendre et maîtriser C++ avec 19 concepts100 exercices, et un vrai mentorat humain, le tout gratuitement.