Hunter Liu's Website

10. Week 5 Tuesday: Functions, Part 1

≪ 9. Week 4 Thursday: Algae Simulator | Table of Contents | 11. Week 5 Thursday: Blackjack ≫

In the past week, you have been exposed to functions in lecture. Over the course of these next two discussions, I hope to accomplish two things: first, I hope we can review the syntax of functions in C++ and common pitfalls with them. Second, I hope we can create something substantial — I decided that creating the casino game blackjack would be a fun project to tackle. This will not only let us get some practise writing real functions, but it will also let us see the role functions play at the level of programming design.

The Philosophy of Functions

Imagine you’re telling someone a recipe for cake. A pretty normal command would be, “turn your oven up to 350 degrees Fahrenheit,” and one has the luxury of assuming that the listening knows how to operate their oven, knows what Fahrenheit means, etc. But imagine how painful it would be if someone asks you, “How do I turn my oven on?” You would have to spend paragraphs describing how to turn the oven dial to the right setting. Not to mention, a particularly aggressive listener might ask, “What if I had an oven without dials?” Et cetera.

This is our predicament with C++ programming, and when we write pseudocode, our expressivity is very limited. The only nouns we are allowed to use are integers, strings, chars, and doubles; the only actions we can perform are arithmetic operations and whatever the C++ libraries say we can do (think sqrt in cmath or substr in string). Some action verbs therefore need to be expounded upon. Think about how pseudocode for blackjack would work: how do you “deal a card”, for instance?

The role of functions is to fill in these gaps in C++’s base vocabrulary. Just as someone else has taught C++ how to squareroot a number or to retrieve a substring, we have the ability to teach C++ how to deal cards, how to shuffle a deck, and how to count score in blackjack.

A good goal to keep when you write code is to make the C++ code as close as possible to pseudocode: complex verbs should be functions that you “teach” C++, which can then be filled in separately. This keeps your code organised and readable for other people. But enough proselytising…let’s review

Function Syntax

A function ought to be thought of as a command or a request to the computer. This request sometimes requires some input information, and it sometimes “returns” to us with some output information. We’ve seen several examples of these:

To write our own function, we need to use the following general syntax:

[return type] [function name]( [parameters] ) {
    [C++ code]
} 

If the function has a return type that’s not void, the function body must have a return statement in it. For instance, the main function always ends with return 0 — we’re telling the function to end itself and supply mysterious external forces with the number 0. Whatever value your function “returns”, it must match the function’s return type.

On the topic of inputs, the way we want a function to handle these inputs may vary depending on the scenario. To illustrate what I mean, consider the following real-world scenario:

You’re working as a secretary at a huge public unversity now, and you need to do a bunch of data processing on the database of student records. You have a very helpful assistant to help you perform some of this data processing, but there is a dilemma. On one hand, you can give them your login to the school database and have them directly do their job. While convenient, this has some security flaws: do you really want a lowly assistant to have unfettered access to all of this sensitive information? What if your assistant screws up and irreversibly messes up decades of student records?

The alternative is to print out or digitally create a copy of the entire school’s database. This fixes the security issues we addressed above, and as a bonus, even if your assistant is a moron and screws everything up, at least you’ll have a backup. However, there are some clear drawbacks as well. Any changes made to the copy of the database must be copied over to the school’s actual database after your assistant is done. This is quite redundant! Moreover, making the copy itself might take a long time, especially if you’re low-tech and want to print it all out on paper!

This is the dilemma we face in C++ when “passing” data into a function. By default, function parameters are passed “by value” — a copy of the inputs are created for the function, so that changes to the inputs from within the function are not reflected outside of the function. For instance, the following program prints the number 5:

 1#include <iostream> 
 2
 3using namespace std; 
 4
 5void increment(int n) {
 6    n = n + 1; 
 7}
 8
 9int main() {
10    int n = 5;
11    increment(n); 
12
13    cout << n << endl; 
14
15    return 0; 
16} 

The n in increment is a copy of the n in main; the two variables are “decoupled”. Changes made to the n in increment do not affect the n in main.

Passing “by reference” is the alternative, where you give a function direct access to a variable that you’re giving it. This is denoted by an ampersand & between the type and name of an input parameter. For instance, consider the following code:

 1#include <iostream> 
 2
 3using namespace std; 
 4
 5void increment(int& n) {
 6    n = n + 1; 
 7}
 8
 9int main() {
10    int n = 5;
11    increment(n); 
12
13    cout << n << endl; 
14
15    return 0; 
16} 

This prints out 6 instead of 5! The n inside increment now “points to” the n inside of main. C++ understands that the two variables represent the same thing, even if they’re in separate functions, and even if you give them different names.

Some Background on Computer Memory, and When Passing by Reference Can Fail

We can sometimes use “literals” — explicit values that could be stored inside variables — as arguments to a function. The 6.25 in sqrt(6.25) is a literal, for instance, and the number returned (2.5) behaves a lot like a literal as well. What happens if I use a literal in a pass-by-reference argument? Will the function have direct access to a literal number, specified only by my code? For instance, what happens in the following code?

 1#include <iostream>
 2
 3using namespace std; 
 4
 5void increment(int& n) {
 6    n = n + 1; 
 7} 
 8
 9int main() {
10    increment(5); 
11    cout << "Hello world!" << endl; 
12
13    return 0;
14} 

The answer is, you get a build error! To understand why, I think it’s a good idea to take a closer look at how C++ programs are represented in computer memory. The following explanation is simplified significantly, but understanding how programs interact with computer memory is paramount to future topics (namely, pointers).

When your computer runs a C++ program, it sections off a little bit of RAM for the main program, just enough space for each of the variables that you use throughout your program. Whenever you call a function, such as sqrt or increment, your computer allocates another chunk of space to hold all the variables in that function.

In addition to the main function and any other functions that get called, your computer also createsa a little chunk of memory to hold the program’s actual instructions! This chunk of memory cannot be modified by the program. This chunk of memory is used to remember all the constants in your code, including the 5 on line 10. So when me write the line increment(5), the compiler understands that we’re asking for direct access to something we shouldn’t ever have access to, thereby resulting in a compilation error.

Blackjack Preliminaries

Let’s now put together some pseudocode and a program skeleton for a game of blackjack. Up until this point, I have been emphasising that pseudocode ought to be easily translatable into C++ code, more or less line for line, sentence for sentence. Let’s put that aside for now and write pseudocode that’s at a human level first. We’ll then identify the information we need to keep track of and how we’ll represent that in C++ variables, then finally identify the “verbs” we’ll need to write functions for.

In case you’re unfamiliar, the game of blackjack (with one player and one dealer) is played as follows:

  1. A standard deck of 52 cards is shuffled.
  2. The player and the dealer are each dealt two cards; one of the dealer’s cards remains face-down, and the rest of the cards are put face up.
  3. If the player’s score is 21, they win immediately (this is called a blackjack).
  4. Otherwise, while the player’s score is less than 21…
    • The player may choose to either hit or stand.
    • If the player chooses to hit, they receive another card.
    • If the player chooses to stand, their turn ends.
  5. If the player has a score of over 21, they bust or lose the game.
  6. The dealer plays. The dealer always hits if their score is less than 17, and they stand otherwise.
  7. If the dealer goes over a score of 21, they bust. Otherwise, the player with a higher score wins.

A player’s cards are scored as follows: number cards receive their face value (e.g. a 3 of clubs is worth 3, a 7 of spades is worth 7, etc.). Face cards are all worth 10 points. Aces can either be worth 11 points, or 1 point if this causes the player to bust.

I apologise if this is a lot to keep track of. However, there is not much that is currently beyond our capabilities: many of these steps are governed by if statements or while loops. Perhaps the only thing we aren’t equipped to handle is the shuffling of a deck…we’ll cross that bridge when we get there.

First and foremost: how are we going to remember the player’s or the dealer’s cards? I think a string is a pretty natural choice: every pair of characters represents a card’s value (A123456789TJQK) followed by its suit (SCHD). So the string "ASKD7C" would correspond to an Ace of Spades, a King of Diamonds, and a 7 of Clubs. Likewise, the deck of cards will be a string of 104 characters, or 52 face value/suit pairs. One can imagine dealing cards amounts to deleting two characters from the front or back of the “deck” string, then adding those two characters to the end of the player’s or the dealer’s hand.

Remark 1. Vectors

Of course, one can represent the deck and the hand as a vector<string>. Everything we do with strings can be done with vector<string>’s, and indeed this is a more natural representation in many ways; for the sake of familiarity, though, I’m choosing to use string’s.

So that’s all as far as what our “memory” encompasses. We just need three strings: a deck of cards, the player’s cards, and the dealer’s cards.

Let’s now make a list of all the significant verbs in the above procedure, which we should think about as functions. For now, we’ll only think about the function signature — what will we name them, what inputs do they take, and what do they return (if anything)? Let’s rewrite the pseudocode in a bit more detail, and I’ll bold all of the nontrivial verbs we need to implement as functions.

  1. Make a string variable called deck with the 52 standard cards, then shuffle the deck.
  2. Create string variables player and dealer. Deal two cards to each.
  3. Compute the player’s score. If it’s 21, they’ve already won — print a message congratulating them and end the game.
  4. While the player’s score is less than 21…
    • Print both hands and scores.
    • Ask the player if they want to hit or stand.
    • If the player wants to hit, deal another card to their hand.
    • If the player wants to stand, end their turn.
  5. Compute the player’s score. If it’s over 21, they’ve lost already — print a message denigrating them and end the game.
  6. While the dealer’s score is less than 17…
    • Print both hands and scores.
    • The dealer hits, so deal a card to the dealer’s hand.
  7. Determine the winner:
    • Compute the dealer’s score and the player’s score.
    • If the dealer’s score is over 21, the player wins.
    • Otherwise, whoever has the highest score wins. If the scores are equal, it’s a tie.

Although there’s a lot that’s been bolded, we can really identify just four verbs we need to worry about.

Shuffling the deck. This should be a function of the form

1void shuffle(string& deck) { /* ... */ } 

This is an action performed on a string variable deck and will change its contents. We want the changes produced by shuffling to persist in the main function, so we keep the input as a reference. We don’t need to return any information, we only need to perform this shuffling action.

Dealing a card to a hand. Like shuffling, this is an action that modifies both the deck of cards (from which a card is removed) and someone’s and (to which a card is added). Thus, our function signature should be

1void deal(string& deck, string& hand) { /* ... */ } 

We are changing these variables wherever they came from! And like shuffle, we don’t need to return any information, hence the void return type.

Computing someone’s score. This needs to receive some cards as an input — that is, a string — but unlike the previous two actions, we’re not changing the hand. Instead, we’re scoring it. We need to return this score, which should be an integer:

1int score(string hand) { /* ... */ } 

Note that I don’t need a different score function for both the player and the dealer, just like I don’t need a different deal function for both the player and the dealer.

Finally, printing out the state of the game. We just need to see both players’ hands, neither of which will be changed during the process of printing everything out. However, note that one of the dealer’s cards will be hidden while the player is playing, but it won’t be hidden later on…we should pass an additional bool that instructs us on whether or not to hide the dealer’s second card.

1void print_game(string player, string dealer, bool hide_dealer) { 
2    /* ... */ 
3} 

We can use these function signatures alongside the pseudocode written above to produce a shell of our program. Every single one of our steps in pseudocode can be expressed using these functions, even if we haven’t yet filled out their bodies!

Try to take this home with you and sleep on it, or start filling things in yourself. The point is that the statement “…and shuffle the deck” is convertible to C++ as shuffle(deck);. Checking the player’s score will be if(score(player) == 21), etc. This allows us to make code that closely resembles our natural language and therefore our pseudocode! Contrast this with what would happen if we didn’t have functions. We would have to worry about all the messy logic that goes into computing each players’ score all 6+ times we want to compute them…that’s just too much.

We’ll actually write out the main program on Thursday (or maybe today if I go fast enough?), and then we can fill out the four actual functions one at a time.