HiveBrain v1.2.0
Get Started
← Back to all entries
principlecppMinor

Newest approach to deck of cards project

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
newestcardsdeckprojectapproach

Problem

As this new approach differs from my previous revisions, it shouldn't need references to them.

After receiving much guidance from CR chat, I ended up utilizing these things:

  • enums for the Rank and Suit instead of arrays



  • a namespace for the enums and Card class



  • to_string()s for representing the Ranks and Suits as std::strings in operator



  • no longer having operator



  • two std::vectors for the Deck class (for retaining the original deck when resetting)



I'd like to know if there's still more that can be done, and I have some questions:

  • Is using two vectors an effective way of preserving the deck (as opposed to giving the consuming code the responsibility to return the cards to the deck)?



  • Should something else be done with the enums in case the user doesn't know which Suits are in which order (when filling the deck with cards)?



  • Although enums are built-in, is const& still good for Card's constructor?



  • Is having the user call empty() on the deck good enough so as to avoid complications with exception-handling within the class?



  • Could the two to_string()s for Rank and Suit be shortened while still resembling a look-up table?



Any other feedback is appreciated as well. Beyond these aspects, I desire overall maintainability for adapting to different games (such as adding a Joker). As such, I don't want to hurt the base design with features only associated with a few games.

Card.h

```
#ifndef CARD_H
#define CARD_H

#include
#include

namespace card
{
enum Rank {Ace,Two,Three,Four,Five,Six,Seven,Eight,Nine,Ten,Jack,Queen,King};
enum Suit {Clubs,Diamonds,Hearts,Spades};

class Card
{
private:
Rank m_rank;
Suit m_suit;

std::string to_string(Rank const&) const;
std::string to_string(Suit const&) const;

public:
Card(Rank const&, Suit const&);
std::string to_string() const;
Rank rank() const {return m_rank;}
Suit suit() const {r

Solution

General notes

-
The to_strings for Rank and Suit do not belong in the Card class. Move them out of the class and into the card namespace instead. They can be useful functions for client code as well.

-
I personally dislike the m_ prefix. Normally you can just use normal variable names instead. If you must have a naming scheme for member variables, I think foo_ is more readable. That way my eyes won't have to discard the m_ before getting to the real name.

-
I'd put spaces inside braces and after commas to increase readability:

bool empty() const { return m_playable.empty(); }
enum Suit { Clubs, Diamonds, Hearts, Spades };


Put the enum definitions on multiple lines if you think the line gets too wide.

-
I'd indent the cases in a switch with only single-line cases. Readability trumps consistency:

switch (suit) {
    case Clubs:    return "C";
    case Diamonds: return "D";
    case Hearts:   return "H";
    case Spades:   return "S";
    default: throw std::logic_error("Invalid Suit");
}


-
"My tests tell me that ..." In C++, where so much is unspecified and undefined, we rely on the standard and/or references, and not on tests. In this case it's easy to be on the safe side:

void Deck::shuffle()
{
    if (m_playable.empty()) return;

    std::random_shuffle(m_playable.begin(), m_playable.end());
}


-
Regarding exception safety for Desk::reset: std::vector::operator= gives the basic exception safety guarantee: If the assignment operator throws, the objects will be in a valid state. Since m_playable already has the required capacity, I can't think of a reason why it should ever throw anyway.

-
Deck::draw() is exception-safe: std::list::pop_back will never throw. See below.

-
Your Card.h header should include ` instead of . Since you only need the std::ostream declaration, you could simply include and include in Card.cpp instead.

-
Is there a reason why op++ and op

-
You should write unit tests and document your interface.

Answers for your questions


Is using two vectors an effective way of preserving the deck (as opposed to giving the consuming code the responsibility to return the cards to the deck)?

I think it is. Asking client code to return the cards is too demanding.


Should something else be done with the
enums in case the user doesn't know which Suits are in which order (when filling the deck with cards)?

The client code should be perfectly able to handle this as it is. If it doesn't want to use your increment operator, it can simply define four loops when populating the deck, or even implement its own function to iterate the suits.


Although
enums are built-in, is const& still good for Card's constructor?

Pass
enums by value.


Is having the user call
empty() on the deck good enough so as to avoid complications with exception-handling within the class?

Yes. As it is,
Deck::draw() has the strong exception safety guarantee: If something goes wrong, it will be rolled back to its original state. In this case you get this for free: std::list::pop_back() has a nothrow guarantee. That means that the only operation in draw() that can fail is calling m_playable.back(), and that doesn't modify the container. As long as that call succeeds, the entire function will as well.


Could the two
to_string()s for Rank and Suit be shortened while still resembling a look-up table?

I think the way you have done it is fine. You could use a
std::map, but that approach has no advantages compared to your approach. If you want to do it anyway, you can do it like this:

std::string to_string(Rank rank)
{
    static const map rank_names = { {Ace, "A"}, {Two, "2"}, {Three, "3"} /* ... */ };

    return rank_names[rank];
}


Note that this has O(n log n) lookup time, whereas your implementation is likely to be O(1). You could also define an array and use the
enum` values as indices. That would also be O(1), but there's still no point -- and that approach violates the abstraction.

Finally, I have a question for you: How would you implement a joker?

Code Snippets

bool empty() const { return m_playable.empty(); }
enum Suit { Clubs, Diamonds, Hearts, Spades };
switch (suit) {
    case Clubs:    return "C";
    case Diamonds: return "D";
    case Hearts:   return "H";
    case Spades:   return "S";
    default: throw std::logic_error("Invalid Suit");
}
void Deck::shuffle()
{
    if (m_playable.empty()) return;

    std::random_shuffle(m_playable.begin(), m_playable.end());
}
std::string to_string(Rank rank)
{
    static const map<Rank, string> rank_names = { {Ace, "A"}, {Two, "2"}, {Three, "3"} /* ... */ };

    return rank_names[rank];
}

Context

StackExchange Code Review Q#29348, answer score: 2

Revisions (0)

No revisions yet.