Partage

[Exercices] Venez vous entraîner !

(v2)

29 octobre 2018 à 16:12:10

EDIT : Ah mince on ne voit plus l'exo avec la limitation de post par page, je le mets en citation du coup :

gbdivers a écrit:

Voici un exercice sur les catégories de valeurs (lvalue, rvalue), la surcharge de fonction avec les lvalue et rvalue references, et le perfect forwarding. En soi, cette serie d'excercices (a faire dans l'ordre) n'est pas complexe si on a compris ces notions (le code a ecrire faire une dizaine de ligne), mais cela permet justement de vérifier si vous avez compris ces notions.

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

1. Ecrire un code qui permet de "voir" les categories de valeur

Modifies le code suivant, de façon a afficher si la fonction f est appélée avec une lvalue ou une rvalue. Il n'est pas nécessaire d'ajouter d'autres include. Il ne faut pas non plus ajouter d'autres fonction que f. (Mais attention, comme dit dans l'introduction, il peut y avoir des surcharges de cette fonction f).

#include <iostream>

// declaration de f
void f(...) { ... }

int main() {
    // appel de f avec une lvalue
    f(...);

    // appel de f avec une rvalue
    f(...);
}

Et il faut que ce code affiche :

f a ete appele avec une lvalue
f a ete appele avec une rvalue

2. La transmission

Modifies le code de facon a ce que f soit appellé via une fonction g (sans modifier le code de f, et en remplaçant uniquement f par g dans main)

#include <iostream>

// declaration de f
void f(...) { ... }

// declaration de g

void g(...) { f(...); } 

int main() {
    // appel de g avec une lvalue
    g(...);

    // appel de g avec une rvalue
    g(...);
}

Quel est le problème dans ce qui est affiché ?

3. Perfect forwarding

Corriger le code en utilisant le perfect forwarding de façon a afficher le résultat attendu.

Donc si j'ai bien compris, le perfect forwarding permet de garder la catégorie de valeur d'un argument, si j'en crois le fail avec le passage de la variable dans g(...) ?

D'ailleurs, pourquoi ça s'est perdu en route ? C'est parce que la variable passée est maintenue localement dans le corps de la fonction avant d'être passée à f(...) ou quelque chose comme ça ?

En tout cas, sympa ce petit exo' qui amène en douceur le concept.

-
Edité par Guit0Xx 29 octobre 2018 à 16:14:47

...
29 octobre 2018 à 16:16:10

Guit0Xx a écrit:

Donc si j'ai bien compris, le perfect forwarding permet de garder la catégorie de valeur d'un argument, si j'en crois le fail avec le passage de la variable dans g(...) ?

Oui.

Guit0Xx a écrit:

D'ailleurs, pourquoi ça s'est perdu en route ? C'est parce que la variable passée est maintenue localement dans le corps de la fonction avant d'être passée à f(...) ou quelque chose comme ça ?

Un paramètre de fonction est une lvalue, une rvalue, ou ca-dépend ?

Pour poser des questions ou simplement discuter informatique, vous pouvez rejoindre le discord NaN.
29 octobre 2018 à 16:19:28

gbdivers a écrit:

Un paramètre de fonction est une lvalue, une rvalue, ou ca-dépend ?

Ça sent la question piège ça, au premier abord j'aurai tendance à dire que ça dépend, mais vu le résultat obtenu précedemment, je dirai réponse A : lvalue.

...
29 octobre 2018 à 16:26:12

Oui. Le critère simple (mais ca fonctionne la plus part du temps) est d'utiliser l'operateur address-of. Est-ce que tu peux le faire sur un paramètre ? Est-ce qu'un paramètre a toujours une adresse mémoire ? A priori, oui. Donc c'est une lvalue.

Pour les details : https://en.cppreference.com/w/cpp/language/value_category (paracétamol non fourni)

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

(je ne peux pas poster un nouveau message pour ajouter un exo, donc je le mets a la suite de ma reponse. Mais c'est indépendant de l'exo précédent)

Une problématique qui revient souvent (à cause de la profusion de mauvais cours C++, malheureusement) est l'utilisation de syntaxes empruntées au C dans un code C++. Il est difficile, quand on n'a pas assez de recul, de comprendre pourquoi ces syntaxes ne sont pas valides en C++. Et surtout que c'est très compliqué d'écrire un code valide en C++ en utilisant ces syntaxes.

Le très gros problème est que les (mauvais) cours n'expliquent pas en quoi ces syntaxes sont problématiques. Quelles erreurs peuvent se produire. Et donc, même si certains cours présentent les exceptions (je ne parlerais même pas des cours qui ne parlent pas de ça), il n'est pas possible d'écrire un code correct si on ne sait pas de quoi il faut se protéger.

Dans cette série d'exercices (à faire dans l'ordre), le but va être de partir de la problématique, d'écrire des tests pour vérifier les erreurs potentielles (approche TDD - Test Driven Development), puis de résoudre les problèmes avec des approches exception-try-catch uniquement. Et terminer sur des approches RAII.

1. Quels sont les problèmes que l'on veut résoudre ?

Le RAII est une solution a plusieurs problèmes. Mais lesquels ? Il faut comprendre les problèmes qui se posent pour trouver une solution pertinente.

Un code simple d'une allocation dynamique d'un objet :

void foo() 
{
    ... pleins de lignes de code ici
    Object* p = new Object();
    ... pleins de lignes de code ici
    p->doSomething();
    ... pleins de lignes de code ici
    delete p;
    ... pleins de lignes de code ici
}

Quels sont les problèmes potentiels ? (On ne sait pas ce que fait "Object" ou "doSomething", ou ce que contient le reste du code. On regarde TOUS les problèmes qui peuvent se poser.)

2. Les tests unitaires

Écrire des tests unitaires qui vont reproduire ces erreurs. (On va fait au plus simple : pas besoin d'utiliser un framework de tests. On va faire 1 test = un programme)

3. Solutions non RAII

Et-il possible de corriger ce code, de façon a résoudre les problèmes ? Sans créer de classe RAII, uniquement avec tr-catch.

4. Solutions RAII

Ecrire une implémentation d'une classe RAII qui résoud ces problèmes. (Prends la semantique de unique_ptr)

5. D'autres exemples

Refaire l'exercice dans le cas d'un tableau dynamique.

void foo() 
{
    ... pleins de lignes de code ici
    Object* p = new[n] Object();
    ... pleins de lignes de code ici
    p[i]->doSomething();
    ... pleins de lignes de code ici
    delete[] p;
    ... pleins de lignes de code ici
}

Refaire l'exercice dans le cas d'un tableau dynamique a 2 dimensions !

void foo() 
{
    ... pleins de lignes de code ici
    Object** p = new[n] Object*();
    ... pleins de lignes de code ici
    p[i]->doSomething();
    ... pleins de lignes de code ici
    delete[] p;
    ... pleins de lignes de code ici
}

Aides !

  • Si vous bloquez sur la première question, quelques mots clés : pointeur invalide (dangling pointer), fuite mémoire (memory leak), double delete.
  • Un article très important à lire pour implémenter une classe RAII : http://www.stroustrup.com/except.pdf 

-
Edité par gbdivers 29 octobre 2018 à 16:40:09

Pour poser des questions ou simplement discuter informatique, vous pouvez rejoindre le discord NaN.
29 octobre 2018 à 21:27:16

Bon pour le moment je n'en suis qu'à la partie non-RAII, et comme je n'avais jamais vraiment utilisé try...catch, je dois dire que ce fût sport (et encore je n'ai pas attaquer le problème avec les tableaux :lol:).

La première chose qui me vient en tête, c'est qu'une exception peut être lancé entre la création de l'objet et le delete, ce qui provoquerait donc une fuite si rien n'est fait pour traiter.

Pour tester, j'ai fait en sorte que la fonction doSomething() balance une exception tout en m'assurant de delete le nécessaire. Ah et j'ai ajouté un pointeur comme membre de Object, histoire d'agrémenter un peu  :

#include <iostream>

////////////////////////////////////////////
class Object
{
public:
    Object(){ a = new int(1); }

    ~Object(){
        if(a != nullptr) delete a;
        std::cout << "Object Destroyed" << '\n';
    };

public:
    void doSomething(int n){
        try{
            if(n == 0)
                throw(std::runtime_error{"Object::doSomething - Cannot be called with value 0"});
        }
        catch(...){
            delete a;
            a = nullptr;
            std::cout << "(int* a) deleted" << '\n';
            throw;
        }
        *a = n;
    };

private:
    int* a{nullptr};
};

////////////////////////////////////////////
void foo()
{
    Object* p = nullptr;
    try{
        p = new Object();
        p->doSomething(0);
        delete p;
        p = nullptr;
    }
    catch(...){
        if(p != nullptr){
            delete p;
            p = nullptr;
        }
        throw;
    }
}

////////////////////////////////////////////
int main()
{
    try{
        foo();
    }
    catch(const std::exception& e){
        std::cout << e.what() << '\n';
    }

    return 0;
}

Ensuite pour le souci du double deleting, j'ai pas trouvé mieux que de surcharger delete (et je suis pas sûr de la validité du bazar):

////////////////////////////////////////////
void operator delete(void* p){
    if(p != nullptr){
        p = nullptr;
        delete(p);
    }
}

////////////////////////////////////////////
int main()
{
    Object* o1 = new Object();
    Object* o2 = o1;
    delete o1;
    delete o2;

    return 0;
}

Bon du coup avant d'aller plus loin, l'ensemble est-il dégueulasse ? Et-ce qu'il y a des choses qui ne sont pas nécessaires ou d'autres qui le sont et que je n'aurai pas vu ?

EDIT : les arrays

#include <iostream>

class Object {};

////////////////////////////////////////////
void foo()
{
    Object* p = nullptr;
    try{
        p = new Object[5];
        delete[] p;
    }
    catch(...){
        delete[] p;
        throw;
    }
}

////////////////////////////////////////////
void bar()
{
    Object** p = nullptr;
    const unsigned rows{3};
    const unsigned cols{3};
    try{
        p = new Object*[rows];
        for(unsigned i = 0; i < rows; ++i){
            p[i] = new Object[cols];
        }
    }
    catch(...){
        for(unsigned i = 0; i < rows; ++i){
			p[i] = nullptr;
            delete[] p[i];
        }
        delete[] p;
        throw;
    }
}

////////////////////////////////////////////
int main()
{
    try{
        foo();
        bar();
    }
    catch(const std::exception& e){
        std::cout << e.what() << '\n';
    }

    return 0;
}



-
Edité par Guit0Xx 30 octobre 2018 à 22:53:57

...
30 octobre 2018 à 21:19:02 - Message modéré pour le motif suivant : Message complètement hors sujet


25 novembre 2018 à 15:40:03

Bonjour , jai un probleme dans un exercice et je vous que vous me aider slv, 

Mon exercice est le suivant :

Ecriver un programme qui trouve les N premier nombre premier avec   1 <= N <= 1000

25 novembre 2018 à 16:31:03

#include <iostream>

static constexpr auto N = 10;

int main() {
    const auto primes = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41,
                          43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97 };
    for (auto it { std::begin(primes) }; it != std::begin(primes)+N; ++it)
        std::cout << (*it) << " ";
}

-
Edité par gbdivers 25 novembre 2018 à 16:31:47

Pour poser des questions ou simplement discuter informatique, vous pouvez rejoindre le discord NaN.
13 décembre 2018 à 12:45:56

Un exercice pour apprendre à debuger un code C++ old school et à le corriger. (Merci au cours C++ du site de nous fournir des codes d'exemples qui posent problèmes !)

La partie POO utilise un code d'exemple d'une classe Personnage, qui ressemble à ça :

#include <iostream>
#include <string>

// ***** Weapon *****
class Weapon { 
public:
    Weapon(std::string name, int damage);
    int damage() const noexcept;
private:
    std::string m_name;
    int m_damage{10};
};

Weapon::Weapon(std::string name, int damage) :
    m_name{std::move(name)}, m_damage{damage}
{}

int Weapon::damage() const noexcept 
{
    return m_damage;
}

// ***** Personnage *****
class Personnage {
public:
    Personnage(std::string name, int damage);
    ~Personnage() noexcept;
private:
    Weapon* m_arme{nullptr};
};
 
Personnage::Personnage(std::string name, int damage) :
    m_arme{new Weapon{std::move(name), damage}}
{
}

Personnage::~Personnage() noexcept
{
    delete m_arme;
    m_arme = nullptr; // inutile!
}

// ***** Main *****
int main()
{
    Personnage* david = new Personnage("épée", 100);
    Personnage* goliath = new Personnage(*david); // on copie david
  
   // use david and goliath
 
    // david est mort, on detruit l'objet
    delete david;
    david = nullptr;
 
    // un nouvel adversaire arrive !
    Personnage* david_survivor = new Personnage("épée", 100);
 
   // use david_survivor and goliath
    
    // clean up
    delete goliath;
    goliath = nullptr;
    
    delete david_survivor;
    david_survivor = nullptr;
  
    return 0;
}

J'ai mit le code un peu à jour, en utilisant des syntaxes du C++11, mais ce n'est pas important ici. Et j'ai volontairement conservé les pointeurs nus, puisque le problème vient de là. (Comme souvent).

Question 1

Que fait ce code ?

Copiez-collez ce code dans compilateur en ligne (Wandbox, coliru ou IDEone) pour voir si vous aviez raison. https://wandbox.org/ 

Question 2

Remplacez les pointeurs nus dans la fonction main par des unique_ptr.

Que fait le code maintenant ?

Question 3

Remplacer les pointeurs nus dans la classe Personnage par des unique_ptr.

Que fait le code maintenant ?

Question 4

Comment corriger ce dernier code, qui utilise que des unique_ptr et plus de pointeurs nus ?

Question 5

Comment corriger le premier code, qui utilise que des pointeurs nus et pas des unique_ptr ?

Pour poser des questions ou simplement discuter informatique, vous pouvez rejoindre le discord NaN.
20 décembre 2018 à 12:25:05

Allez je me lance !

Question 1 :

Ce programme créé 2 Personnages "identiques", avec un constructeur par données (pas de souci de ce côté là) et un constructeur par copie (non spécifié, alors que la classe Personnage manipule un pointeur ..).
Utilisation des 2 instances créées.
On décide de détruire l'une des 2 instances créées avec l'opérateur delete => PROBLEME : on détruit l'arme pointée par david ET par Goliath.
En effet, les 2 instances de Personnage pointent vers la même arme :(
On décide de créer une autre instance de Personnage avec un constructeur par données (pas de problème sauf si l'allocation échoue :( ).
Ensuite, on décide de détruire goliath => plantage car on fait un double delete !

Il semblerait que coliru me donne raison lors de l'exécution.

*** Error in `./a.out': double free or corruption (fasttop): 0x0000000001ceac20 ***

Question 2 :

Vu que je n'ai jamais utilisé les RAII, je m'aide du site référence pour le C++ ( http://www.cplusplus.com )

Après une petite recherche sur le constructeur de la classe std::unique_ptr, et sur son "destructeur" (ou plutôt la méthode release) et sur la bibliothèque à insérer (ici <memory> ), j'obtiens le code suivant :

#include <iostream>
#include <string>
#include <memory>

// ***** Weapon *****
class Weapon {
public:
    Weapon(std::string name, int damage);
    int damage() const noexcept;
private:
    std::string m_name;
    int m_damage{10};
};

Weapon::Weapon(std::string name, int damage) :
        m_name{std::move(name)}, m_damage{damage}
{}

int Weapon::damage() const noexcept
{
    return m_damage;
}

// ***** Personnage *****
class Personnage {
public:
    Personnage(std::string name, int damage);
    ~Personnage() noexcept;
private:
    Weapon* m_arme{nullptr};
};

Personnage::Personnage(std::string name, int damage) :
        m_arme{new Weapon{std::move(name), damage}}
{
}

Personnage::~Personnage() noexcept
{
    delete m_arme;
    m_arme = nullptr; // inutile!
}

// ***** Main *****
int main()
{
    std::unique_ptr<Personnage> david ( new Personnage("épée", 100) );
    std::unique_ptr<Personnage> goliath( new Personnage(*david) );  // on copie david

    // use david and goliath

    // david est mort, on detruit l'objet
    david.release();

    // un nouvel adversaire arrive !
    std::unique_ptr<Personnage> david_survivor ( new Personnage("épée", 100) );


    // use david_survivor and goliath

    // clean up
    goliath.release();

    david_survivor.release();

    return 0;
}
Après une 1ère exécution, je n'obtiens plus d'erreurs de double libération de mémoire.
Ca signifie donc que le problème à la ligne 61 du code de la Q1 a été réglé.
J'imagine que l'appel au constructeur de la classe std::unique_ptr à la ligne 48 correspond au cas numéro 3 décrit dans la documentation http://www.cplusplus.com/reference/memory/unique_ptr/unique_ptr/
Quant à comprendre ce qui écrit pour le cas numéro 3, je remarque que "stored pointer" signifie "pointeur nu". Mais j'ai du mal à expliquer ce qu'il se passe véritable avec le pointeur sur Weapon.

Question 3 :

On remplace tous les pointeurs nus de la classe Personnage par un unique_ptr.
On obtient ainsi le code suivant :
#include <iostream>
#include <string>
#include <memory>
 
// ***** Weapon *****
class Weapon {
public:
    Weapon(std::string name, int damage);
    int damage() const noexcept;
private:
    std::string m_name;
    int m_damage{10};
};
 
Weapon::Weapon(std::string name, int damage) :
        m_name{std::move(name)}, m_damage{damage}
{}
 
int Weapon::damage() const noexcept
{
    return m_damage;
}
 
// ***** Personnage *****
class Personnage {
public:
    Personnage(std::string name, int damage);
    ~Personnage() noexcept;
private:
    // Weapon* m_arme{nullptr};
    std::unique_ptr<Weapon> m_arme {nullptr};
};
 
Personnage::Personnage(std::string name, int damage) :
        m_arme{new Weapon{std::move(name), damage}}
{
}
 
Personnage::~Personnage() noexcept
{
    m_arme.release();
}
 
// ***** Main *****
int main()
{
    std::unique_ptr<Personnage> david ( new Personnage("épée", 100) );
    std::unique_ptr<Personnage> goliath( new Personnage(*david) );  // on copie david
 
    // use david and goliath
 
    // david est mort, on detruit l'objet
    david.release();
 
    // un nouvel adversaire arrive !
    std::unique_ptr<Personnage> david_survivor ( new Personnage("épée", 100) );
 
 
    // use david_survivor and goliath
 
    // clean up
    goliath.release();
 
    david_survivor.release();
 
    return 0;
}


Le problème, c'est que ça compile pas :/

main.cpp:48:46: error: call to implicitly-deleted copy constructor of 'Personnage'

    std::unique_ptr<Personnage> goliath( new Personnage(*david) );  // on copie david


Cette erreur est très certainement due à l'affectation d'un unique_ptr sur un autre unique_ptr dans le constructeur par copie de la classe Personnage.

Question 4 :

Etant donné qu'un type d'arme peut être utilisée par plusieurs personnages (david et goliath utilisent chacun une épée.), on a 2 solutions :
- Soit on arrête d'utiliser les pointeurs, et on compte sur la RAM de l'utilisateur pour contenir les nombreuses copies d'une épée.
- Soit on utilise l'optimisation du C++, autrement dit on utilise les shared_ptr.
La deuxième solution est la meilleure en terme de gains de performances.

Question 5 :

Si l'on veut rester sur les pointeurs nus, on peut utiliser la solution made in OC qui est de redéfinir le constructeur par copie de la classe Personnage, en faisant un appel au constructeur par copie de la classe Weapon pour l'attribut m_arme.
Puisque la classe Weapon n'utilise pas de pointeurs, il n'est pas nécessaire de redéfinir son constructeur par copie.



-
Edité par Dropper 20 décembre 2018 à 14:02:39

[Exercices] Venez vous entraîner !

× Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
  • Editeur
  • Markdown