Un stack sur une classe abstraite

L'auteur de ce sujet a trouvé une solution à son problème.
Auteur du sujet

Bonjour !

J'essaye d'implémenter (en C++) une classe deck qui est, en gros, une pile de cards.

1
2
3
4
5
class deck {
private:
    std::stack<card*> stack;
//...
};

Seulement, la classe card est abstraite, et je dois donc utiliser des pointeurs. Or, je sais que C++ (depuis C++11 notamment) propose des outils pour éviter les pointeurs nus.

C'est là qu'est mon problème : je me suis jamais servi de ces smart pointers, et je ne sais pas trop comment m'y prendre… Est ce que je dois faire de stack un std::stack<std::unique_ptr<card> > ? Et qu'est-ce que ça change pour mes méthodes comme draw(), pour piocher ? Est-ce que je dois retourner un std::unique_ptr<card> ?

Merci pour votre aide !

Cette réponse a aidé l'auteur du sujet

Lu'!

Si ton deck est l'unique responsable des cartes qu'il contient jusqu'à ce que la carte soit retirée, et qu'alors il transmet cette responsabilité, c'est un unique_ptr que tu dois stocker puis transmettre. Tu auras sûrement besoin de deux fonctions de unique_ptr : le swap et la construction par déplacement.

Édité par Ksass`Peuk

First : Always RTFM - "Tout devrait être rendu aussi simple que possible, mais pas plus." A.Einstein

+1 -0
Auteur du sujet

Il va également y avoir un std::vector<std::unique_ptr<card> > pour représenter la main du joueur, mais, par définition, les cartes qui sont dans la main ne sont pas dans le deck.

Comment ça se passe du coup, ce "transfert de responsabilité" ?

EDIT : Je me suis un peu renseigné sur la rule of three, la copie par assignation et ce genre de trucs. C'est très intéressant, mais dans mon cas, une carte n'est responsable d'aucune ressource.

En revanche, les cartes présentes dans un deck ne seront probablement pas créées par le deck et leur ownership devra être transféré à la main du joueur une fois la carte piochée. Est-ce que je dois surcharger certains opérateurs, ou laisser ceux par défaut faire le boulot ?

EDIT 2 : Je viens de tomber sur std::move dans la doc, qui clarifie pas mal ta réponse, et la manière dont tout cela va se combiner. :)

Édité par Richou D. Degenne

Cette réponse a aidé l'auteur du sujet

Tu peux transférer une responsabilité en déplaçant celle-ci. Si tu ne connais pas la sémantique des movements, je t'invite à lire le tuto qui a été fait à ce sujet : Une nouvelle fonctionnalité de C++11 : la sémantique de mouvement. Je t'ai mis quelques exemples rapides en dessous pour que tu puisses voir comment l'utiliser dans ton cas.

En fait, tu vas simplement faire quasiment comme avec un pointeur nu, mais en faisant attention à ne jamais appeler le constructeur de copie (qui est supprimé).

Par exemple, si tu veux prendre une carte sur ton deck, tu vas faire quelque chose comme :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class deck {
  std::stack<std::unique_ptr<card>> stack;
  std::unique_ptr<card> get_top() {
    // on récupère la responsabilité
    std::unique_ptr<card> c = std::move(stack.top());
    // on supprime le haut de la pile
    stack.pop();
    // on transfère la responsabilité à l'appelant
    return std::move(c);
  }
}

int main() {
   deck d;
   // on remplis le deck...

   // ici, deck.get_top() retourne un std::unique_ptr<card>&& qui est utilisé pour construire c
   std::unique_ptr<card> c = deck.get_top();
}

Tu vas donc devoir appeler std::move quand tu veux récupérer la responsabilité d'un pointeur et la transférer.

Dans le cas où tu voudrais échanger deux cartes, il y a aussi std::swap :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
std::vector<card> deck1;
std::vector<card> deck2;

// échange la première carte des deux decks
std::swap(deck1[0], deck2[0]);

// équivalent à
std::unique_ptr<card> tmp = std::move(deck1[0]);
deck1[0] = std::move(deck2[0]);
deck2[0] = std::move(tmp);

Attention, une fois que tu as déplacé la responsabilité (avec std::move), il ne faut surtout pas utiliser l'ancien pointeur. Il est donc préférable qu'il soit détruit assez rapidement.

EDIT : et j'allais oublier, pour ajouter une carte en haut de ton deck, c'est aussi un poil particulier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class deck {
  std::stack<std::unique_ptr<card>> stack;
  void put_top(std::unique_ptr<card> &&c) {
    stack.push(std::move(c));
  }
}

int main() {
  std::unique_ptr<card> c;
  deck d;
  // tu met ce que tu veux pour la carte et le deck

  d.put_top(std::move(c));
  // à partir d'ici, il ne faut plus utiliser c
}

Édité par Berdes

+1 -0
Auteur du sujet

Mais c'est que je vais commencer à faire du C++ proprement avec tout ça ! :lol:

Bon, en tous cas, ce qui me rassure, c'est que le code que tu proposes ressemble beaucoup à ce vers quoi j'étais en train de m'orienter, donc je crois que j'ai compris le bousin. :) Et merci pour le tutoriel, je savais même pas qu'on avait ça ! :o

Je ne vais pas marquer le sujet résolu de suite, y'a des chances que je sois encore un peu paumé par-ci par-là avec tout cette nouveauté ! :P

Auteur du sujet

Je me permets le double-post, le dernier message remontant à hier :3

Toujours à propos de cette fameuse sémantique de mouvement, j'ai dans la classe deck une méthode add_card pour ajouter une carte au deck.

Si j'ai bien compris le schmilblick, comme c'est le deck qui va récupérer l'ownership de la carte, la méthode va ressembler à quelque chose dans ce genre-là ?

1
2
3
void deck::add_card(std::unique_ptr<card> && new_card) {
    stack.push(new std::unique_ptr<card> {new_card});
}

Quelques questions : quid du pointeur source ? Je laisse l'utilisateur s'occuper de sa destruction ? Et je suis pas sûr du new, mais je vois pas trop comment l'écrire autrement… :(

Ben non pas de new, puisque tu n'alloues rien de neuf, tu te contentes de conserver quelque chose de déjà alloué :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <cassert>
#include <vector>
#include <memory>

using namespace std;

class Foo{
public:
  void add(std::unique_ptr<int>&& p){
    data.push_back(std::move(p));  
  }
  std::unique_ptr<int> get(){
    assert(!data.empty());

    std::unique_ptr<int> res{};
    res.swap(data.back());
    data.pop_back();
    return std::move(res);
  }

private:
  std::vector<std::unique_ptr<int>> data;
};

int main(){
  Foo f;
  f.add(std::make_unique<int>(42));
  auto p = f.get();

  std::cout<<*p<<std::endl;
}

First : Always RTFM - "Tout devrait être rendu aussi simple que possible, mais pas plus." A.Einstein

+1 -0

Là, en fait, tu insères dans ta stack un pointeur "brut" sur un nouveau pointeur unique_ptr sur une card. Il faut plutôt faire quelque chose du genre :

1
2
3
void deck::add_card(std::unique_ptr<card> && new_card) {
    stack.push(new_card);
}

std::stack::push possède une surcharge qui prend une rvalue en paramètre, permettant de déplacer le pointeur new_card (donc son ownership) dans la std::stack.

+0 -0

Accéder hors d'un tableau c'est une erreur de programmation, pas une erreur due à l'environnement d'exécution, accéder à un stack vide, c'est pareil. Si le paquet est vide, il ne devrait même pas y avoir de possibilité de faire un appel au retrait d'un élément.

Si le paquet est vide, le joueur ne doit même pas avoir d'option "piocher" de disponible.

Édité par Ksass`Peuk

First : Always RTFM - "Tout devrait être rendu aussi simple que possible, mais pas plus." A.Einstein

+0 -0

Non, c'est à ton logiciel et désactiver la possibilité de piocher quand le deck est vide. Ou si tu marches par commande, à ton logiciel de regarder si c'est possible de piocher avant de le faire réellement.

L'interface de dialogue c'est pas Joueur -> Deck. C'est au minimum Joueur -> Contrôle -> Deck.

  • Si le joueur saisit "Pioche", tu vas aller vérifier que le paquet contient des cartes, vérifier si les règles l'autorise, puis faire effectivement la pioche, il n'y a pas de scénario d'exception là dedans.
  • Si le joueur saisit "iPoceh", tu vas chercher dans tes commandes si une telle commande existe et si elle n'existe pas dire "je n'ai pas compris, retape", pas d'exception non plus là dedans.
  • Si le joueur balance un Ctrl-D, là par contre c'est pas trop normal comme scénario, et tu ne peux rien traiter du coup, cette fois, on aurait éventuellement à faire à une exception. Sauf évidemment si tu as prévu de traiter un "Ctrl-D" comme étant l'arrêt du jeu et il n'y aura pas d'exception, tu remontes juste l'information qui sert à quitter.

Édité par Ksass`Peuk

First : Always RTFM - "Tout devrait être rendu aussi simple que possible, mais pas plus." A.Einstein

+1 -0

Pas besoin de d'embêter avec std::move dans le return car vu que tu lui retourne un objet temporaire, le compilateur va automatiquement le considérer comme une rvalue (même si le type de retour est l'objet, pas une rvalue sur l'objet).

Le code que tu avais au début est bon pour cette méthode

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  std::unique_ptr<card> get_top() {
    std::unique_ptr<card> c = std::move(stack.top());
    stack.pop();
    // pas besoin de std::move, on retourne un objet temporaire (c) dont le type est identique au
    // type de retour de la fonction => le compilateur le considère donc comme un rvalue. 
    // pas besoin de faire std::move dans le code appelant : le compilateur considérant le valeur
    // de retour comme une rvalue (malgré les apparences ^^), il tentera d'utiliser le constructeur
    // de mouvement (std::unique_ptr<card> maCarte = monDeck.get_top() marchera correctement)
    return c;
  }
+0 -0
Auteur du sujet

Ok pour le move dans la fonction de pioche. En revanche, dans le code appelant, j'ai été obligé de préciser le move. J'avais écrit ça :

1
cards.push_back(library.draw());

(où cards est un std::vector<std::unique_ptr<card> > et library un deck) et le compilateur a résolu ça avec le constructeur par recopie qui a été supprimé plutôt qu'avec le constructeur par déplacement. Du coup, je l'ai remplacé par ça :

1
cards.push_back(std::move(library.draw()));

pour "forcer" l'utilisation du constructeur par déplacement.

Oui, j'avais oublié de préciser que ma réponse était dans le cas où tu faisais ceci :

1
std::unique<int> monPtr = maStack.get_top();

Dans le code ci-dessus, le compilateur va utiliser le constructeur de mouvement (privilégié car il doit résoudre un constructeur).

Par contre, dans ton exemple de code, le compilateur va résoudre l'appel à la fonction push_back de vector et va privilégier le surcharge avec une référence lvalue traditionnelle. D'où l'intérêt de mettre std::move pour le forcer à prendre la surcharge avec la rvalue en paramètre.

EDIT : c'est quand même assez complexe comme sujet, il y a plein de petites subtilités ^^

Édité par victorlevasseur

+1 -0
Vous devez être connecté pour pouvoir poster un message.
Connexion

Pas encore inscrit ?

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