principlecppMinor
Newest approach to deck of cards project
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:
I'd like to know if there's still more that can be done, and I have some questions:
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
After receiving much guidance from CR chat, I ended up utilizing these things:
enums for theRankandSuitinstead of arrays
- a
namespacefor theenums andCardclass
to_string()s for representing the Ranks and Suits asstd::strings inoperator
- no longer having operator
- two
std::vectors for theDeckclass (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, isconst&still good forCard'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
-
I personally dislike the
-
I'd put spaces inside braces and after commas to increase readability:
Put the
-
I'd indent the
-
"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:
-
Regarding exception safety for
-
-
Your
Finally, I have a question for you: How would you implement a joker?
-
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.