Chapter Review
Fixed-size arrays (or fixed-length arrays) require that the length of the array be known at the point of instantiation, and that length cannot be changed afterward. C-style arrays and std::array
are both fixed-size arrays. Dynamic arrays can be resized at runtime. std::vector
is a dynamic array.
The length of a std::array
must be a constant expression. Most often, the value provided for the length will be an integer literal, constexpr variable, or an unscoped enumerator.
std::array
is an aggregate. This means it has no constructors, and instead is initialized using aggregate initialization.
Define your std::array
as constexpr whenever possible. If your std::array
is not constexpr, consider using a std::vector
instead.
Use class template argument deduction (CTAD) to have the compiler deduce the type and length of a std::array from its initializers.
std::array
is implemented as a template struct whose declaration looks like this:
template<typename T, std::size_t N> // N is a non-type template parameter
struct array;
The non-type template parameter representing the array length (N
) has type std::size_t
.
To get the length of a std::array
:
- We can ask a
std::array
object for its length using the size()
member function (which returns the length as unsigned size_type
).
- In C++17, we can use the
std::size()
non-member function (which for std::array
just calls the size()
member function, thus returning the length as unsigned size_type
).
- In C++20, we can use the
std::ssize()
non-member function, which returns the length as a large signed integral type (usuallystd::ptrdiff_t
).
All three of these functions will return the length as a constexpr value, except when called on a std::array
passed by reference. This defect has been addressed in C++23 by P2280.
To index a std::array
:
- Use the subscript operator (
operator[]
). No bounds checking is done in this case, and passing in an invalid index will result in undefined behavior.
- Use the
at()
member function that does subscripting with runtime bounds checking. We recommend avoiding this function since we typically want to do bounds checking before indexing, or we want compile-time bounds checking.
- Use the
std::get()
function template, which takes the index as a non-type template argument, and does compile-time bounds checking.
You can pass std::array
with different element types and lengths to a function using a function template with the template parameter declaration template <typename T, std::size_t N>
. Or in C++20, use template <typename T, auto N>
.
Returning a std::array
by value will make a copy of the array and all elements, but this may be okay if the array is small and the elements aren’t expensive to copy. Using an out parameter instead may be a better choice in some contexts.
When initializing a std::array
with a struct, class, or array and not providing the element type with each initializer, you’ll need an extra pair of braces so that the compiler will properly interpret what to initialize. This is an artifact of aggregate initialization, and other standard library container types (that use list constructors) do not require the double braces in these cases.
Aggregates in C++ support a concept called brace elision, which lays out some rules for when multiple braces may be omitted. Generally, you can omit braces when initializing a std::array
with scalar (single) values, or when initializing with class types or arrays where the type is explicitly named with each element.
You can not have an array of references, but you can have an array of std::reference_wrapper
, which behaves like a modifiable lvalue reference.
There are a few things worth noting about std::reference_wrapper
:
Operator=
will reseat a std::reference_wrapper
(change which object is being referenced).
std::reference_wrapper<T>
will implicitly convert to T&
.
- The
get()
member function can be used to get a T&
. This is useful when we want to update the value of the object being referenced.
The std::ref()
and std::cref()
functions were provided as shortcuts to create std::reference_wrapper
and const std::reference_wrapper
wrapped objects.
Use static_assert
whenever possible to ensure a constexpr std::array
using CTAD has the correct number of initializers.
C-style arrays were inherited from the C language, and are built-in to the core language of C++. Because they are part of the core language, C-style arrays have their own special declaration syntax. In an C-style array declaration, we use square brackets ([]) to tell the compiler that a declared object is a C-style array. Inside the square brackets, we can optionally provide the length of the array, which is an integral value of type std::size_t that tells the compiler how many elements are in the array. The length of a C-style array must be a constant expression.
C-style arrays are aggregates, which means they can be initialized using aggregate initialization. When using an initializer list to initialize all elements of a C-style array, it’s preferable to omit the length and let the compiler calculate the length of the array.
C-style arrays can be indexed via operator[]
. The index of a C-style array can be either a signed or an unsigned integer, or an unscoped enumeration. This means that C-style arrays are not subject to all of the sign conversion indexing issues that the standard library container classes have!
C-style arrays can be const or constexpr.
To get the length of a C-style array:
- In C++17, we can use the
std::size()
non-member function, which returns the length as unsigned std::size_t
.
- In C++20, we can use the
std::ssize()
non-member function, which returns the length as a large signed integral type (usuallystd::ptrdiff_t
).
In most cases, when a C-style array is used in an expression, the array will be implicitly converted into a pointer to the element type, initialized with the address of the first element (with index 0). Colloquially, this is called array decay (or just decay for short).
Pointer arithmetic is a feature the allows us to apply certain integer arithmetic operators (addition, subtraction, increment, or decrement) to a pointer to produce a new memory address. Given some pointer ptr
, ptr + 1
returns the address of the next object in memory (based on the type being pointed to).
Use subscripting when indexing from the start of the array (element 0), so the array indices line up with the element.
Use pointer arithmetic when doing relative positioning from a given element.
C-style strings are just C-style arrays whose element type is char
or const char
. As such, C-style strings will decay.
The dimension of an array is the number of indices needed to select an element.
An array containing only a single dimension is called a single-dimensional array or a one-dimensional array (sometimes abbreviated as a 1d array). An array of arrays is called a two-dimensional array (sometimes abbreviated as a 2d array) because it has two subscripts. Arrays with more than one dimension are called multidimensional arrays. Flattening an array is a process of reducing the dimensionality of an array (often down to a single dimension).
In C++23, std::mdspan
is a view that provides a multidimensional array interface for a contiguous sequence of elements.
Quiz time
Question #1
What’s wrong with each of these snippets, and how would you fix it?
a)
#include <array>
#include <iostream>
int main()
{
std::array arr { 0, 1, 2, 3 };
for (std::size_t count{ 0 }; count <= std::size(arr); ++count)
{
std::cout << arr[count] << ' ';
}
std::cout << '\n';
return 0;
}
Show Solution
The for-loop has an off-by-one error, and tries to access the array element with index 4, which does not exist.
Solution: the conditional in the for loop should use < instead of <=.
b)
#include <iostream>
void printArray(int array[])
{
for (int element : array)
{
std::cout << element << ' ';
}
}
int main()
{
int array[] { 9, 7, 5, 3, 1 };
printArray(array);
std::cout << '\n';
return 0;
}
Show Solution
array
decays to a pointer when it is passed to printArray()
. Range-based for-loops can’t work with a pointer to an array because the size of the array isn’t known.
Solution: use std::array
instead, which doesn’t decay.
c)
#include <array>
#include <iostream>
int main()
{
std::cout << "Enter the number of test scores: ";
std::size_t length{};
std::cin >> length;
std::array<int, length> scores;
for (std::size_t i { 0 } ; i < length; ++i)
{
std::cout << "Enter score " << i << ": ";
std::cin >> scores[i];
}
return 0;
}
Show Solution
length
is not a constant expression, and cannot be used to define the length of std::array
.
Solution: use a std::vector
instead.
Question #2
In this quiz, we’re going to implement Roscoe’s potion emporium, the finest potion shop in the land! This is going to be a bigger challenge.
Implement a program that outputs the following:
Welcome to Roscoe's potion emporium!
Enter your name: Alex
Hello, Alex, you have 85 gold.
Here is our selection for today:
0) healing costs 20
1) mana costs 30
2) speed costs 12
3) invisibility costs 50
Enter the number of the potion you'd like to buy, or 'q' to quit: a
That is an invalid input. Try again: 3
You purchased a potion of invisibility. You have 35 gold left.
Here is our selection for today:
0) healing costs 20
1) mana costs 30
2) speed costs 12
3) invisibility costs 50
Enter the number of the potion you'd like to buy, or 'q' to quit: 4
That is an invalid input. Try again: 2
You purchased a potion of speed. You have 23 gold left.
Here is our selection for today:
0) healing costs 20
1) mana costs 30
2) speed costs 12
3) invisibility costs 50
Enter the number of the potion you'd like to buy, or 'q' to quit: 2
You purchased a potion of speed. You have 11 gold left.
Here is our selection for today:
0) healing costs 20
1) mana costs 30
2) speed costs 12
3) invisibility costs 50
Enter the number of the potion you'd like to buy, or 'q' to quit: 4
You can not afford that.
Here is our selection for today:
0) healing costs 20
1) mana costs 30
2) speed costs 12
3) invisibility costs 50
Enter the number of the potion you'd like to buy, or 'q' to quit: q
Your inventory contains:
2x potion of speed
1x potion of invisibility
You escaped with 11 gold remaining.
Thanks for shopping at Roscoe's potion emporium!
The player starts with a randomized amount of gold, between 80 and 120.
Sound fun? Let’s do it! Because this will be hard to implement all at once, we’ll develop this in steps.
> Step #1
Create a Potion
namespace containing an enum named Type
containing the potion types. Create two std::array
: an int
array to hold the potion costs, and a std::string_view
array to hold the potion names.
Also write a function named shop()
that enumerates through the list of Potions
and prints their numbers, names, and cost.
The program should output the following:
Here is our selection for today:
0) healing costs 20
1) mana costs 30
2) speed costs 12
3) invisibility costs 50
Show Hint
Show Solution
#include <array>
#include <iostream>
#include <string_view>
namespace Potion
{
enum Type
{
healing,
mana,
speed,
invisibility,
max_potions
};
constexpr std::array types { healing, mana, speed, invisibility }; // An array of our enumerators
// We could put these in a struct, but since we only have two attributes we'll keep them separate for now
// We will explicitly define the element type so we don't have to use the sv suffix
constexpr std::array<std::string_view, max_potions> name { "healing", "mana", "speed", "invisibility" };
constexpr std::array cost { 20, 30, 12, 50 };
static_assert(std::size(types) == max_potions); // ensure 'all' contains the correct number of enumerators
static_assert(std::size(cost) == max_potions);
static_assert(std::size(name) == max_potions);
}
void shop()
{
std::cout << "Here is our selection for today:\n";
for (auto p: Potion::types)
std::cout << p << ") " << Potion::name[p] << " costs " << Potion::cost[p] << '\n';
}
int main()
{
shop();
return 0;
}
> Step #2
Create a Player
class to store the player’s name, potion inventory, and gold. Add the introductory and goodbye text for Roscoe’s emporium. Get the player’s name and randomize their gold.
Use the “Random.h” file in lesson 8.15 -- Global random numbers (Random.h) to make randomization easy.
The program should output the following:
Welcome to Roscoe's potion emporium!
Enter your name: Alex
Hello, Alex, you have 84 gold.
Here is our selection for today:
0) healing costs 20
1) mana costs 30
2) speed costs 12
3) invisibility costs 50
Thanks for shopping at Roscoe's potion emporium!
Show Solution
#include <array>
#include <iostream>
#include <string_view>
#include "Random.h"
namespace Potion
{
enum Type
{
healing,
mana,
speed,
invisibility,
max_potions
};
constexpr std::array types { healing, mana, speed, invisibility }; // An array of our enumerators
// We could put these in a struct, but since we only have two attributes we'll keep them separate for now
// We will explicitly define the element type so we don't have to use the sv suffix
constexpr std::array<std::string_view, max_potions> name { "healing", "mana", "speed", "invisibility" };
constexpr std::array cost { 20, 30, 12, 50 };
static_assert(std::size(types) == max_potions); // ensure 'all' contains the correct number of enumerators
static_assert(std::size(cost) == max_potions);
static_assert(std::size(name) == max_potions);
}
class Player
{
private:
static constexpr int s_minStartingGold { 80 };
static constexpr int s_maxStartingGold { 120 };
std::string m_name {};
int m_gold {};
std::array<int, Potion::max_potions> m_inventory { };
public:
explicit Player(std::string_view name) :
m_name { name },
m_gold { Random::get(s_minStartingGold, s_maxStartingGold) }
{
}
int gold() const { return m_gold; }
int inventory(Potion::Type p) const { return m_inventory[p]; }
};
void shop()
{
std::cout << "Here is our selection for today:\n";
for (auto p: Potion::types)
std::cout << p << ") " << Potion::name[p] << " costs " << Potion::cost[p] << '\n';
}
int main()
{
std::cout << "Welcome to Roscoe's potion emporium!\n";
std::cout << "Enter your name: ";
std::string name{};
std::getline(std::cin >> std::ws, name); // read a full line of text into name
Player player { name };
std::cout << "Hello, " << name << ", you have " << player.gold() << " gold.\n\n";
shop();
std::cout << "\nThanks for shopping at Roscoe's potion emporium!\n";
return 0;
}
> Step #3
Add the ability to purchase potions, handling invalid input (treat any extraneous input as a failure). Print the player’s inventory after they leave. The program should be complete after this step.
Make sure you test for the following cases:
- User enters an invalid potion number (e.g. ‘d’)
- User enters a valid potion number but with extraneous input (e.g.
2d
, 25
)
We cover invalid input handling in lesson 9.5 -- std::cin and handling invalid input.
Show Hint
Hint: The user can enter either a number or ‘q’, so extract the user’s input to type
char
.
To convert an ASCII number character to an int (e.g. '5'
to 5
), you can use the following:
int charNumToInt(char c)
{
return c - '0';
}
Show Hint
Hint: Write a function to handle user input. It should return the Potion::Type
the user selected. If the user quit instead, the function can return Potion::max_potions
. You will need to static_cast the user’s input to a Potion::Type
.
Show Solution
#include <array>
#include <iostream>
#include <limits> // for std::numeric_limits
#include <string_view>
#include "Random.h"
namespace Potion
{
enum Type
{
healing,
mana,
speed,
invisibility,
max_potions
};
constexpr std::array types { healing, mana, speed, invisibility }; // An array of our enumerators
// We could put these in a struct, but since we only have two attributes we'll keep them separate for now
// We will explicitly define the element type so we don't have to use the sv suffix
constexpr std::array<std::string_view, max_potions> name { "healing", "mana", "speed", "invisibility" };
constexpr std::array cost { 20, 30, 12, 50 };
static_assert(std::size(types) == max_potions); // ensure 'all' contains the correct number of enumerators
static_assert(std::size(cost) == max_potions);
static_assert(std::size(name) == max_potions);
}
class Player
{
private:
static constexpr int s_minStartingGold { 80 };
static constexpr int s_maxStartingGold { 120 };
std::string m_name {};
int m_gold {};
std::array<int, Potion::max_potions> m_inventory { };
public:
explicit Player(std::string_view name) :
m_name { name },
m_gold { Random::get(s_minStartingGold, s_maxStartingGold) }
{
}
// returns false if can't afford, true if purchased
bool buy(Potion::Type type)
{
if (m_gold < Potion::cost[type])
return false;
m_gold -= Potion::cost[type];
++m_inventory[type];
return true;
}
int gold() const { return m_gold; }
int inventory(Potion::Type p) const { return m_inventory[p]; }
};
void ignoreLine()
{
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
int charNumToInt(char c)
{
return c - '0';
}
Potion::Type whichPotion()
{
std::cout << "Enter the number of the potion you'd like to buy, or 'q' to quit: ";
char input{};
while (true)
{
std::cin >> input;
if (!std::cin)
{
std::cin.clear(); // put us back in 'normal' operation mode
ignoreLine(); // and remove the bad input
continue;
}
// If there is extraneous input, treat as failure case
if (!std::cin.eof() && std::cin.peek() != '\n')
{
std::cout << "I didn't understand what you said. Try again: ";
ignoreLine(); // ignore any extraneous input
continue;
}
if (input == 'q')
return Potion::max_potions;
// Convert the char to a number and see if it's a valid potion selection
int val { charNumToInt(input) };
if (val >= 0 && val < Potion::max_potions)
return static_cast<Potion::Type>(val);
// It wasn't a valid potion selection
std::cout << "I didn't understand what you said. Try again: ";
ignoreLine();
}
}
void shop(Player &player)
{
while (true)
{
std::cout << "Here is our selection for today:\n";
for (auto p: Potion::types)
std::cout << p << ") " << Potion::name[p] << " costs " << Potion::cost[p] << '\n';
Potion::Type which { whichPotion() };
if (which == Potion::max_potions)
return;
bool success { player.buy(which) };
if (!success)
std::cout << "You can not afford that.\n\n";
else
std::cout << "You purchased a potion of " << Potion::name[which] << ". You have " << player.gold() << " gold left.\n\n";
}
}
void printInventory(Player& player)
{
std::cout << "Your inventory contains: \n";
for (auto p: Potion::types)
{
if (player.inventory(p) > 0)
std::cout << player.inventory(p) << "x potion of " << Potion::name[p] << '\n';
}
std::cout << "You escaped with " << player.gold() << " gold remaining.\n";
}
int main()
{
std::cout << "Welcome to Roscoe's potion emporium!\n";
std::cout << "Enter your name: ";
std::string name{};
std::cin >> name;
Player player { name };
std::cout << "Hello, " << name << ", you have " << player.gold() << " gold.\n\n";
shop(player);
std::cout << '\n';
printInventory(player);
std::cout << "\nThanks for shopping at Roscoe's potion emporium!\n";
return 0;
}
Question #3
Let’s say we want to write a card game that uses a standard deck of cards. In order to do that, we’re going to need some way to represent those cards, and decks of cards. Let’s build that functionality.
We’ll use it in the next quiz question to actually implement a game.
> Step #1
A deck of cards has 52 unique cards (13 card ranks of 4 suits). Create enumerations for the card ranks (ace, 2, 3, 4, 5, 6, 7, 8, 9, 10, jack, queen, king) and suits (clubs, diamonds, hearts, spades).
Show Solution
// Because identifiers can't start with a number, we'll use a "rank_" prefix for these
enum Rank
{
rank_ace,
rank_2,
rank_3,
rank_4,
rank_5,
rank_6,
rank_7,
rank_8,
rank_9,
rank_10,
rank_jack,
rank_queen,
rank_king,
max_ranks
};
// We'll also prefix these for consistency
enum Suit
{
suit_club,
suit_diamond,
suit_heart,
suit_spade,
max_suits
};
> Step #2
Each card will be represented by a struct named Card
that contains a rank and a suit member. Create the struct and move the enums into it.
Show Solution
struct Card
{
enum Rank
{
rank_ace,
rank_2,
rank_3,
rank_4,
rank_5,
rank_6,
rank_7,
rank_8,
rank_9,
rank_10,
rank_jack,
rank_queen,
rank_king,
max_ranks
};
enum Suit
{
suit_club,
suit_diamond,
suit_heart,
suit_spade,
max_suits
};
Rank rank{};
Suit suit{};
};
> Step #3
Next, let’s add some useful functions to our Card struct. First, overload operator<<
to print the card rank and suit as a 2-letter code (e.g. the jack of spades would print as JS). You can do that by completing the following function:
struct Card
{
// Your other stuff here
friend std::ostream& operator<<(std::ostream& out, const Card &card)
{
out << // print your card rank and suit here
return out;
}
};
Second, add a function that returns the value of the Card. Treat an ace as value 11. Finally, add a std::array
of Rank and of Suit (named allRanks
and allSuits
respectively) so they can be iterated over. Because these are part of a struct (not a namespace), make them static so they are only instantiated once (not with each object).
The following should compile:
int main()
{
// Print one card
Card card { Card::rank_5, Card::suit_heart };
std::cout << card << '\n';
// Print all cards
for (auto suit : Card::allSuits)
for (auto rank : Card::allRanks)
std::cout << Card { rank, suit } << ' ';
std::cout << '\n';
return 0;
}
and produce the following output:
5H
AC 2C 3C 4C 5C 6C 7C 8C 9C TC JC QC KC AD 2D 3D 4D 5D 6D 7D 8D 9D TD JD QD KD AH 2H 3H 4H 5H 6H 7H 8H 9H TH JH QH KH AS 2S 3S 4S 5S 6S 7S 8S 9S TS JS QS KS
Show Solution
#include <array>
#include <iostream>
struct Card
{
enum Rank
{
rank_ace,
rank_2,
rank_3,
rank_4,
rank_5,
rank_6,
rank_7,
rank_8,
rank_9,
rank_10,
rank_jack,
rank_queen,
rank_king,
max_ranks
};
enum Suit
{
suit_club,
suit_diamond,
suit_heart,
suit_spade,
max_suits
};
// These need to be static so they are only created once per program, not once per Card
static constexpr std::array allRanks { rank_ace, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7, rank_8, rank_9, rank_10, rank_jack, rank_queen, rank_king };
static constexpr std::array allSuits { suit_club, suit_diamond, suit_heart, suit_spade };
Rank rank{};
Suit suit{};
friend std::ostream& operator<<(std::ostream& out, const Card &card)
{
static constexpr std::array ranks { 'A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K' };
static constexpr std::array suits { 'C', 'D', 'H', 'S' };
out << ranks[card.rank] << suits[card.suit];
return out;
}
int value() const
{
static constexpr std::array rankValues { 11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10 };
return rankValues[rank];
}
};
int main()
{
// Print one card
Card card { Card::rank_5, Card::suit_heart };
std::cout << card << '\n';
// Print all cards
for (auto suit : Card::allSuits)
for (auto rank : Card::allRanks)
std::cout << Card { rank, suit } << ' ';
std::cout << '\n';
return 0;
}
> Step #4
Next, let’s create our deck of cards. Create a class named Deck
that contains a std::array
of Cards. You can assume a deck is 52 Cards.
The Deck should have three functions:
First, the default constructor should initialize the array of cards. You can use a ranged-for loop similar to the one in the main() function of the prior example to traverse through all the suits and ranks.
Second, add a dealCard()
function that returns the next card in the Deck by value. Since std::array
is a fixed-size array, think about how you will keep track of where the next card is. This function should assert out if it is called when the Deck has gone through all the cards.
Third, write a shuffle()
member function that shuffles the deck. To make this easy, we will enlist the help of std::shuffle
:
#include <algorithm> // for std::shuffle
#include "Random.h" // for Random::mt
// Put this line in your shuffle function to shuffle m_cards using the Random::mt Mersenne Twister
// This will rearrange all the Cards in the deck randomly
std::shuffle(m_cards.begin(), m_cards.end(), Random::mt);
The shuffle()
function should also reset however you are tracking where the next card is back to the start of the deck.
The following program should run:
int main()
{
Deck deck{};
std::cout << deck.dealCard() << ' ' << deck.dealCard() << ' ' << deck.dealCard() << '\n';
deck.shuffle();
std::cout << deck.dealCard() << ' ' << deck.dealCard() << ' ' << deck.dealCard() << '\n';
return 0;
}
and produce the following output (the last 3 cards should be randomized):
AC 2C 3C
2H 7H 9C
Show Solution
#include <algorithm> // for std::shuffle
#include <array>
#include <cassert>
#include <iostream>
#include "Random.h"
struct Card
{
enum Rank
{
rank_ace,
rank_2,
rank_3,
rank_4,
rank_5,
rank_6,
rank_7,
rank_8,
rank_9,
rank_10,
rank_jack,
rank_queen,
rank_king,
max_ranks
};
enum Suit
{
suit_club,
suit_diamond,
suit_heart,
suit_spade,
max_suits
};
static constexpr std::array allRanks { rank_ace, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7, rank_8, rank_9, rank_10, rank_jack, rank_queen, rank_king };
static constexpr std::array allSuits { suit_club, suit_diamond, suit_heart, suit_spade };
Rank rank{};
Suit suit{};
friend std::ostream& operator<<(std::ostream& out, const Card &card)
{
static constexpr std::array ranks { 'A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K' };
static constexpr std::array suits { 'C', 'D', 'H', 'S' };
out << ranks[card.rank] << suits[card.suit];
return out;
}
int value() const
{
static constexpr std::array rankValues { 11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10 };
return rankValues[rank];
}
};
class Deck
{
private:
std::array<Card, 52> m_cards {};
std::size_t m_nextCardIndex { 0 };
public:
Deck()
{
std::size_t count { 0 };
for (auto suit: Card::allSuits)
for (auto rank: Card::allRanks)
m_cards[count++] = Card{rank, suit};
}
void shuffle()
{
std::shuffle(m_cards.begin(), m_cards.end(), Random::mt);
m_nextCardIndex = 0;
}
Card dealCard()
{
assert(m_nextCardIndex != 52 && "Deck::dealCard ran out of cards");
return m_cards[m_nextCardIndex++];
}
};
int main()
{
Deck deck{};
std::cout << deck.dealCard() << ' ' << deck.dealCard() << ' ' << deck.dealCard() << '\n';
deck.shuffle();
std::cout << deck.dealCard() << ' ' << deck.dealCard() << ' ' << deck.dealCard() << '\n';
return 0;
}
Question #4
Alright, now let’s use our Card and Deck to implement a simplified version of Blackjack! If you’re not already familiar with Blackjack, the Wikipedia article for Blackjack has a summary.
Here are the rules for our version of Blackjack:
- The dealer gets one card to start (in real life, the dealer gets two, but one is face down so it doesn’t matter at this point).
- The player gets two cards to start.
- The player goes first.
- A player can repeatedly “hit” or “stand”.
- If the player “stands”, their turn is over, and their score is calculated based on the cards they have been dealt.
- If the player “hits”, they get another card and the value of that card is added to their total score.
- An ace normally counts as a 1 or an 11 (whichever is better for the total score). For simplicity, we’ll count it as an 11 here.
- If the player goes over a score of 21, they bust and lose immediately.
- When the player is done, it is the dealer’s turn.
- The dealer repeatedly draws until they reach a score of 17 or more, at which point they must stop drawing.
- If the dealer goes over a score of 21, they bust and the player wins immediately.
- Otherwise, if the player has a higher score than the dealer, the player wins. Otherwise, the player loses (we’ll consider ties as dealer wins for simplicity).
In our simplified version of Blackjack, we’re not going to keep track of which specific cards the player and the dealer have been dealt. We’ll only track the sum of the values of the cards they have been dealt for the player and dealer. This keeps things simpler.
Start with the code you wrote in the prior quiz (or use our reference solution).
> Step #1
Create a struct named Player
that will represent a participant in our game (either the dealer or the player). Since in this game we only care about a player’s score, this struct only needs one member.
Write a function that will (eventually) play a round of Blackjack. For now, this function should draw one randomized card for the dealer and two randomized cards for the player. It should return a bool value indicating who has the greater score.
The code should output the following:
The dealer is showing: 10
You have score: 13
You win!
The dealer is showing: 10
You have score: 8
You lose!
Show Solution
#include <algorithm> // for std::shuffle
#include <array>
#include <cassert>
#include <iostream>
#include "Random.h"
struct Card
{
enum Rank
{
rank_ace,
rank_2,
rank_3,
rank_4,
rank_5,
rank_6,
rank_7,
rank_8,
rank_9,
rank_10,
rank_jack,
rank_queen,
rank_king,
max_ranks
};
enum Suit
{
suit_club,
suit_diamond,
suit_heart,
suit_spade,
max_suits
};
static constexpr std::array allRanks { rank_ace, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7, rank_8, rank_9, rank_10, rank_jack, rank_queen, rank_king };
static constexpr std::array allSuits { suit_club, suit_diamond, suit_heart, suit_spade };
Rank rank{};
Suit suit{};
friend std::ostream& operator<<(std::ostream& out, const Card &card)
{
static constexpr std::array ranks { 'A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K' };
static constexpr std::array suits { 'C', 'D', 'H', 'S' };
out << ranks[card.rank] << suits[card.suit];
return out;
}
int value() const
{
static constexpr std::array rankValues { 11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10 };
return rankValues[rank];
}
};
class Deck
{
private:
std::array<Card, 52> m_cards {};
std::size_t m_nextCardIndex { 0 };
public:
Deck()
{
std::size_t count { 0 };
for (auto suit: Card::allSuits)
for (auto rank: Card::allRanks)
m_cards[count++] = Card{rank, suit};
}
void shuffle()
{
std::shuffle(m_cards.begin(), m_cards.end(), Random::mt);
m_nextCardIndex = 0;
}
Card dealCard()
{
assert(m_nextCardIndex != 52 && "Deck::dealCard ran out of cards");
return m_cards[m_nextCardIndex++];
}
};
struct Player
{
int score{};
};
bool playBlackjack()
{
Deck deck{};
deck.shuffle();
Player dealer{ deck.dealCard().value() };
std::cout << "The dealer is showing: " << dealer.score << '\n';
Player player { deck.dealCard().value() + deck.dealCard().value() };
std::cout << "You have score: " << player.score << '\n';
return (player.score > dealer.score);
}
int main()
{
if (playBlackjack())
{
std::cout << "You win!\n";
}
else
{
std::cout << "You lose!\n";
}
return 0;
}
> Step #2
Add a Settings
namespace that contains two constants: the value above which the player busts, and the value where the dealer must stop drawing cards.
Add the logic that handles the dealer’s turn. The dealer will draw cards until they hit 17, then they must stop. If they bust, the player wins.
Here is some sample output:
The dealer is showing: 8
You have score: 9
The dealer flips a 4D. They now have: 12
The dealer flips a JS. They now have: 22
The dealer went bust!
You win!
The dealer is showing: 6
You have score: 13
The dealer flips a 3D. They now have: 9
The dealer flips a 3H. They now have: 12
The dealer flips a 9S. They now have: 21
You lose!
The dealer is showing: 7
You have score: 21
The dealer flips a JC. They now have: 17
You win!
Show Solution
#include <algorithm> // for std::shuffle
#include <array>
#include <cassert>
#include <iostream>
#include "Random.h"
struct Card
{
enum Rank
{
rank_ace,
rank_2,
rank_3,
rank_4,
rank_5,
rank_6,
rank_7,
rank_8,
rank_9,
rank_10,
rank_jack,
rank_queen,
rank_king,
max_ranks
};
enum Suit
{
suit_club,
suit_diamond,
suit_heart,
suit_spade,
max_suits
};
static constexpr std::array allRanks { rank_ace, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7, rank_8, rank_9, rank_10, rank_jack, rank_queen, rank_king };
static constexpr std::array allSuits { suit_club, suit_diamond, suit_heart, suit_spade };
Rank rank{};
Suit suit{};
friend std::ostream& operator<<(std::ostream& out, const Card &card)
{
static constexpr std::array ranks { 'A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K' };
static constexpr std::array suits { 'C', 'D', 'H', 'S' };
out << ranks[card.rank] << suits[card.suit];
return out;
}
int value() const
{
static constexpr std::array rankValues { 11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10 };
return rankValues[rank];
}
};
class Deck
{
private:
std::array<Card, 52> m_cards {};
std::size_t m_nextCardIndex { 0 };
public:
Deck()
{
std::size_t count { 0 };
for (auto suit: Card::allSuits)
for (auto rank: Card::allRanks)
m_cards[count++] = Card{rank, suit};
}
void shuffle()
{
std::shuffle(m_cards.begin(), m_cards.end(), Random::mt);
m_nextCardIndex = 0;
}
Card dealCard()
{
assert(m_nextCardIndex != 52 && "Deck::dealCard ran out of cards");
return m_cards[m_nextCardIndex++];
}
};
struct Player
{
int score{};
};
namespace Settings
{
// Maximum score before losing.
constexpr int bust{ 21 };
// Minium score that the dealer has to have.
constexpr int dealerStopsAt{ 17 };
}
// Returns true if the dealer went bust. False otherwise.
bool dealerTurn(Deck& deck, Player& dealer)
{
while (dealer.score < Settings::dealerStopsAt)
{
Card card { deck.dealCard() };
dealer.score += card.value();
std::cout << "The dealer flips a " << card << ". They now have: " << dealer.score << '\n';
}
if (dealer.score > Settings::bust)
{
std::cout << "The dealer went bust!\n";
return true;
}
return false;
}
bool playBlackjack()
{
Deck deck{};
deck.shuffle();
Player dealer{ deck.dealCard().value() };
std::cout << "The dealer is showing: " << dealer.score << '\n';
Player player { deck.dealCard().value() + deck.dealCard().value() };
std::cout << "You have score: " << player.score << '\n';
if (dealerTurn(deck, dealer))
return true;
return (player.score > dealer.score);
}
int main()
{
if (playBlackjack())
{
std::cout << "You win!\n";
}
else
{
std::cout << "You lose!\n";
}
return 0;
}
> Step #3
Finally, add logic for the player’s turn. This will complete the game.
Here is some sample output:
The dealer is showing: 2
You have score: 14
(h) to hit, or (s) to stand: h
You were dealt KH. You now have: 24
You went bust!
You lose!
The dealer is showing: 10
You have score: 9
(h) to hit, or (s) to stand: h
You were dealt TH. You now have: 19
(h) to hit, or (s) to stand: s
The dealer flips a 3D. They now have: 13
The dealer flips a 7H. They now have: 20
You lose!
The dealer is showing: 7
You have score: 12
(h) to hit, or (s) to stand: h
You were dealt 7S. You now have: 19
(h) to hit, or (s) to stand: h
You were dealt 2D. You now have: 21
(h) to hit, or (s) to stand: s
The dealer flips a 6H. They now have: 13
The dealer flips a QC. They now have: 23
The dealer went bust!
You win!
Show Solution
#include <algorithm> // for std::shuffle
#include <array>
#include <cassert>
#include <iostream>
#include "Random.h"
struct Card
{
enum Rank
{
rank_ace,
rank_2,
rank_3,
rank_4,
rank_5,
rank_6,
rank_7,
rank_8,
rank_9,
rank_10,
rank_jack,
rank_queen,
rank_king,
max_ranks
};
enum Suit
{
suit_club,
suit_diamond,
suit_heart,
suit_spade,
max_suits
};
static constexpr std::array allRanks { rank_ace, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7, rank_8, rank_9, rank_10, rank_jack, rank_queen, rank_king };
static constexpr std::array allSuits { suit_club, suit_diamond, suit_heart, suit_spade };
Rank rank{};
Suit suit{};
friend std::ostream& operator<<(std::ostream& out, const Card &card)
{
static constexpr std::array ranks { 'A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K' };
static constexpr std::array suits { 'C', 'D', 'H', 'S' };
out << ranks[card.rank] << suits[card.suit];
return out;
}
int value() const
{
static constexpr std::array rankValues { 11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10 };
return rankValues[rank];
}
};
class Deck
{
private:
std::array<Card, 52> m_cards {};
std::size_t m_nextCardIndex { 0 };
public:
Deck()
{
std::size_t count { 0 };
for (auto suit: Card::allSuits)
for (auto rank: Card::allRanks)
m_cards[count++] = Card{rank, suit};
}
void shuffle()
{
std::shuffle(m_cards.begin(), m_cards.end(), Random::mt);
m_nextCardIndex = 0;
}
Card dealCard()
{
assert(m_nextCardIndex != 52 && "Deck::dealCard ran out of cards");
return m_cards[m_nextCardIndex++];
}
};
struct Player
{
int score{};
};
namespace Settings
{
// Maximum score before losing.
constexpr int bust{ 21 };
// Minium score that the dealer has to have.
constexpr int dealerStopsAt{ 17 };
}
bool playerWantsHit()
{
while (true)
{
std::cout << "(h) to hit, or (s) to stand: ";
char ch{};
std::cin >> ch;
switch (ch)
{
case 'h':
return true;
case 's':
return false;
}
}
}
// Returns true if the player went bust. False otherwise.
bool playerTurn(Deck& deck, Player& player)
{
while (player.score < Settings::bust && playerWantsHit())
{
Card card { deck.dealCard() };
player.score += card.value();
std::cout << "You were dealt " << card << ". You now have: " << player.score << '\n';
}
if (player.score > Settings::bust)
{
std::cout << "You went bust!\n";
return true;
}
return false;
}
// Returns true if the dealer went bust. False otherwise.
bool dealerTurn(Deck& deck, Player& dealer)
{
while (dealer.score < Settings::dealerStopsAt)
{
Card card { deck.dealCard() };
dealer.score += card.value();
std::cout << "The dealer flips a " << card << ". They now have: " << dealer.score << '\n';
}
if (dealer.score > Settings::bust)
{
std::cout << "The dealer went bust!\n";
return true;
}
return false;
}
bool playBlackjack()
{
Deck deck{};
deck.shuffle();
Player dealer{ deck.dealCard().value() };
std::cout << "The dealer is showing: " << dealer.score << '\n';
Player player { deck.dealCard().value() + deck.dealCard().value() };
std::cout << "You have score: " << player.score << '\n';
if (playerTurn(deck, player))
return false;
if (dealerTurn(deck, dealer))
return true;
return (player.score > dealer.score);
}
int main()
{
if (playBlackjack())
{
std::cout << "You win!\n";
}
else
{
std::cout << "You lose!\n";
}
return 0;
}
Question #5
a) Describe how you could modify the above program to handle the case where aces can be equal to 1 or 11.
It’s important to note that we’re only keeping track of the sum of the cards, not which specific cards the user has.
Show Solution
One way would be to keep track of how many aces the player and the dealer got dealt (In the Player
struct
, as an integer). If either the player or dealer go over 21 and their ace counter is greater than zero, you can reduce their score by 10 (convert an ace from 11 points to 1 point) and “remove” one from the ace counter. This can be done as many times as needed until the ace counter reaches zero.
b) In actual blackjack, if the player and dealer have the same score (and the player has not gone bust), the result is a tie and neither wins. Describe how you’d modify the above program to account for this.
Show Solution
Our version of playBlackjack()
currently returns a bool indicating whether the player won or not. We’ll need to update this function to return three possibilities: Dealer win, Player win, tie. The best way to do this would be to define an enumeration for these three options, and have the function return the appropriate enumerator.
c) Extra credit: implement the above two ideas into your blackjack game. Note that you will need to show the dealer’s initial card and the player’s initial two cards so they know whether they have an ace or not.
Here’s a sample output:
The dealer is showing JH (10)
You are showing AH 7D (18)
(h) to hit, or (s) to stand: h
You were dealt JD. You now have: 18
(h) to hit, or (s) to stand: s
The dealer flips a 6C. They now have: 16
The dealer flips a AD. They now have: 17
You win!
Show Solution
#include <algorithm> // for std::shuffle
#include <array>
#include <cassert>
#include <iostream>
#include "Random.h"
struct Card
{
enum Rank
{
rank_ace,
rank_2,
rank_3,
rank_4,
rank_5,
rank_6,
rank_7,
rank_8,
rank_9,
rank_10,
rank_jack,
rank_queen,
rank_king,
max_ranks
};
enum Suit
{
suit_club,
suit_diamond,
suit_heart,
suit_spade,
max_suits
};
static constexpr std::array allRanks { rank_ace, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7, rank_8, rank_9, rank_10, rank_jack, rank_queen, rank_king };
static constexpr std::array allSuits { suit_club, suit_diamond, suit_heart, suit_spade };
Rank rank{};
Suit suit{};
friend std::ostream& operator<<(std::ostream& out, const Card &card)
{
static constexpr std::array ranks { 'A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K' };
static constexpr std::array suits { 'C', 'D', 'H', 'S' };
out << ranks[card.rank] << suits[card.suit];
return out;
}
int value() const
{
static constexpr std::array rankValues { 11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10 };
return rankValues[rank];
}
};
class Deck
{
private:
std::array<Card, 52> m_cards {};
std::size_t m_nextCardIndex { 0 };
public:
Deck()
{
std::size_t count { 0 };
for (auto suit: Card::allSuits)
for (auto rank: Card::allRanks)
m_cards[count++] = Card{rank, suit};
}
void shuffle()
{
std::shuffle(m_cards.begin(), m_cards.end(), Random::mt);
m_nextCardIndex = 0;
}
Card dealCard()
{
assert(m_nextCardIndex != 52 && "Deck::dealCard ran out of cards");
return m_cards[m_nextCardIndex++];
}
};
class Player
{
private:
int m_score{ };
int m_aceCount { 0 };
public:
// We'll use a function to add the card to the player's score
// Since we now need to count aces
void addToScore(Card card)
{
m_score += card.value();
if (card.rank == Card::rank_ace)
++m_aceCount;
}
void consumeAces(int maxScore)
{
while (m_score > maxScore && m_aceCount > 0)
{
m_score -= 10;
--m_aceCount;
}
}
int score() { return m_score; }
};
namespace Settings
{
// Maximum score before losing.
constexpr int bust{ 21 };
// Minium score that the dealer has to have.
constexpr int dealerStopsAt{ 17 };
}
bool playerWantsHit()
{
while (true)
{
std::cout << "(h) to hit, or (s) to stand: ";
char ch{};
std::cin >> ch;
switch (ch)
{
case 'h':
return true;
case 's':
return false;
}
}
}
// Returns true if the player went bust. False otherwise.
bool playerTurn(Deck& deck, Player& player)
{
while (player.score() < Settings::bust && playerWantsHit())
{
Card card { deck.dealCard() };
player.addToScore(card);
player.consumeAces(Settings::bust);
std::cout << "You were dealt " << card << ". You now have: " << player.score() << '\n';
}
if (player.score() > Settings::bust)
{
std::cout << "You went bust!\n";
return true;
}
return false;
}
// Returns true if the dealer went bust. False otherwise.
bool dealerTurn(Deck& deck, Player& dealer)
{
while (dealer.score() < Settings::dealerStopsAt)
{
Card card { deck.dealCard() };
dealer.addToScore(card);
dealer.consumeAces(Settings::bust);
std::cout << "The dealer flips a " << card << ". They now have: " << dealer.score() << '\n';
}
if (dealer.score() > Settings::bust)
{
std::cout << "The dealer went bust!\n";
return true;
}
return false;
}
enum class GameResult
{
playerWon,
dealerWon,
tie
};
GameResult playBlackjack()
{
Deck deck{};
deck.shuffle();
Player dealer{};
Card card1 { deck.dealCard() };
dealer.addToScore(card1);
std::cout << "The dealer is showing " << card1 << " (" << dealer.score() << ")\n";
Player player{};
Card card2 { deck.dealCard() };
Card card3 { deck.dealCard() };
player.addToScore(card2);
player.addToScore(card3);
std::cout << "You are showing " << card2 << ' ' << card3 << " (" << player.score() << ")\n";
if (playerTurn(deck, player)) // if player busted
return GameResult::dealerWon;
if (dealerTurn(deck, dealer)) // if dealer busted
return GameResult::playerWon;
if (player.score() == dealer.score())
return GameResult::tie;
return (player.score() > dealer.score() ? GameResult::playerWon : GameResult::dealerWon);
}
int main()
{
switch (playBlackjack())
{
case GameResult::playerWon:
std::cout << "You win!\n";
return 0;
case GameResult::dealerWon:
std::cout << "You lose!\n";
return 0;
case GameResult::tie:
std::cout << "It's a tie.\n";
return 0;
}
return 0;
}