Structs and Enums
Every type you have used so far came with the language or its library: int, double, bool, char, std::string, std::vector. This lesson is about making types of your own.
There are two kinds. A struct groups several values into one. An enum class is a type with a short list of named values and no others. Together they let a program describe the thing it is about, such as a position, a student or a game board, in one variable of one type.
Values that belong together
A position on the canvas is two numbers. Until now a program kept them in two variables, such as door_x and door_y, and nothing but their names said that they belong together. A function that needs two positions takes four ints, and a function cannot return a position at all, because it returns one value.
A struct makes the pair into one thing.
#include <iostream>
struct Spot {
int x;
int y;
};
int main() {
Spot door{120, 250};
Spot window{300, 120};
std::cout << "Two spots made\n";
}
Two spots made
The five lines above main are a struct definition. It has these parts.
| Part | What it is |
|---|---|
struct |
the word that starts the definition |
Spot |
the name of the new type. This course starts the names of its own types with a capital letter, so that a type and a variable are easy to tell apart |
{ ... } |
the members: the values every Spot holds. Each is written like a declaration, with a type and a name |
; |
a semicolon after the closing brace |
That last semicolon is easy to forget, because the closing brace of a function has none. When it is missing the compiler rejects the program, and its message often names the line after the definition.
The definition creates a type. It does not create a variable, and it holds no numbers. It only says what a Spot is: an int called x and an int called y.
Variables come from declarations, as always. Spot door{120, 250}; reads: the type Spot, the name door, and in braces a value for each member. The values go to the members in the order the definition lists them, so door holds an x of 120 and a y of 250. window is a second Spot, with an x and a y of its own.
So this program has four ints, two inside each variable. It prints nothing about them yet, because the next section is about how to reach a member.
One member at a time
To use one member, write the variable, a dot, and the member's name.
#include "scimigo.h"
using namespace scimigo;
struct Spot {
int x;
int y;
};
int main() {
canvas(600, 400);
Spot ball{100, 250};
circle(ball.x, ball.y, 25, "orange");
ball.x = ball.x + 150;
circle(ball.x, ball.y, 25, "orange");
ball.x = ball.x + 150;
ball.y = 120;
circle(ball.x, ball.y, 25, "red");
}
ball.x is "the x inside ball". It is an int variable in every way: it can be read, passed to a function, assigned to and updated. ball.x = ball.x + 150; is the read, work out, store of module 2, done to one member. The other member is not touched.
Two things do not work. x on its own means nothing in main: there is no variable of that name, only a member of some Spot, and the compiler has to be told which one. And Spot.x is wrong, because Spot is a type and holds no values. The dot always follows a variable.
A whole struct can be copied with =, and the copy is independent of the original.
#include <iostream>
struct Spot {
int x;
int y;
};
int main() {
Spot a{3, 4};
Spot b = a;
b.x = 50;
std::cout << a.x << " " << a.y << "\n";
std::cout << b.x << " " << b.y << "\n";
a = b;
std::cout << a.x << " " << a.y << "\n";
}
3 4
50 4
50 4
Spot b = a; copies every member of a into the new variable b. Changing b.x afterwards leaves a alone, as it would for two ints. The assignment a = b; copies every member again, in the other direction.
Giving every member a value
Module 2 warned about int n;. A variable given no value holds whatever was in memory, and reading it is a mistake. The same is true of Spot s;. It has two members and neither has been given a value. This course never writes that line.
There are three ways to make sure that every member has a value.
#include <iostream>
#include <string>
struct Spot {
int x;
int y;
};
struct Player {
std::string name = "nobody";
int lives = 3;
int score = 0;
};
int main() {
Spot origin{};
std::cout << origin.x << " " << origin.y << "\n";
Player guest{};
Player ada{"Ada"};
Player max{"Max", 5, 120};
std::cout << guest.name << " " << guest.lives << " " << guest.score << "\n";
std::cout << ada.name << " " << ada.lives << " " << ada.score << "\n";
std::cout << max.name << " " << max.lives << " " << max.score << "\n";
guest.name = "Lin";
guest.score = 40;
std::cout << guest.name << " " << guest.lives << " " << guest.score << "\n";
}
0 0
nobody 3 0
Ada 3 0
Max 5 120
Lin 3 40
Empty braces. Spot origin{}; gives every member the zero of its type: 0 for a number, false for a bool, the empty text for a string. The two characters {} are the difference between a Spot that is safe to read and one that is not.
Default member values. Player gives each member a value in the definition itself: int lives = 3;. This is the value a member gets whenever the braces say nothing about it. Player guest{}; therefore starts as nobody, with 3 lives and a score of 0.
Values in the braces. They fill the members from the first one, in the order of the definition, and names play no part in it. Player ada{"Ada"} gives a value to name only. lives and score keep their defaults. Player max{"Max", 5, 120} gives all three. You cannot skip a member: there is no way to write "the score is 120, and leave the rest". The braces can only stop early.
A value of the wrong type, or more values than there are members, is a compile error. Player p{5}; does not compile, because the first member is a string and 5 is not text.
After that, any member can be changed with an assignment, as the last lines do.
Make a habit of the braces. Write Spot s{}; even when the next lines set both members. Then no member is ever without a value.
Passing a struct to a function
A struct is passed to a function in the same three ways as any other value. Module 10 showed them for an int and a vector.
#include <iostream>
struct Spot {
int x;
int y;
};
void print_spot(const Spot& spot) {
std::cout << "(" << spot.x << ", " << spot.y << ")\n";
}
void move_right(Spot& spot, int by) {
spot.x = spot.x + by;
}
Spot below(Spot spot, int by) {
spot.y = spot.y + by;
return spot;
}
int main() {
Spot lamp{40, 60};
print_spot(lamp);
move_right(lamp, 100);
print_spot(lamp);
Spot shadow = below(lamp, 200);
print_spot(shadow);
print_spot(lamp);
print_spot(Spot{7, 9});
}
(40, 60)
(140, 60)
(140, 260)
(140, 60)
(7, 9)
| Parameter | What the function gets | Use it when |
|---|---|---|
Spot spot |
a copy of the caller's struct, every member | the function needs a copy of its own to change |
Spot& spot |
the caller's own variable under another name | the function is there to change the caller's struct |
const Spot& spot |
the caller's variable, to look at only | the function only reads it. This is the usual choice |
move_right takes a Spot&, so spot.x = ... inside it changes lamp in main. The second line of output shows it.
below takes a copy. It changes the copy's y and returns the copy. A function returns one value, and that value can be a whole struct. main stores it in shadow. The fourth line of output shows that lamp was not changed by the call.
print_spot takes a const Spot&. Nothing is copied, and the compiler rejects any statement in its body that would change a member.
The last call passes Spot{7, 9}. A type's name followed by braces makes a value of that type without giving it a name, in the way 7 is an int with no name. Such a value can be given to a Spot or a const Spot& parameter. It cannot be given to a Spot& parameter, because there is no variable for the function to change.
Many of them
A vector holds many values of one type, and that type can be a struct. This is how a program keeps a list of things that each have several properties.
#include <iostream>
#include <string>
#include <vector>
struct Parcel {
std::string to;
int grams = 0;
};
int main() {
std::vector<Parcel> van = {{"Lisbon", 800}, {"Oslo", 2300}};
van.push_back(Parcel{"Turin", 450});
Parcel late{"Cork", 1200};
van.push_back(late);
for (Parcel& parcel : van) {
parcel.grams += 50;
}
int total = 0;
for (const Parcel& parcel : van) {
std::cout << parcel.to << ": " << parcel.grams << "\n";
total += parcel.grams;
}
std::cout << "Parcels: " << van.size() << "\n";
std::cout << "First: " << van[0].to << "\n";
std::cout << "Total: " << total << "\n";
}
Lisbon: 850
Oslo: 2350
Turin: 500
Cork: 1250
Parcels: 4
First: Lisbon
Total: 4950
The type of van is std::vector<Parcel>: a vector whose elements are Parcels. In the list it starts with, each inner pair of braces is one Parcel. push_back adds a whole Parcel at the end, either a value made on the spot or a copy of a variable.
Both loops are the loops of module 10. for (Parcel& parcel : van) makes parcel another name for each element in turn, so adding 50 grams of packaging changes the elements. With Parcel parcel in that place the loop would change copies and the vector would stay as it was. for (const Parcel& parcel : van) looks at each element without copying it.
van[0].to is read from left to right. van[0] is the first element, which is a Parcel. The dot then takes its member to.
The other way to store this data is two vectors, one of names and one of weights, where position 2 of each belongs to the same parcel. That falls apart as soon as one of them is sorted, or gets an element the other does not. In a vector of structs a parcel's name and weight stay together because they are one element.
A type with a few named values
Some values are one of a few possibilities. The weather in a simple game is sunny, cloudy or rainy. You could store that in an int and agree that 0 means sunny. Then nothing stops the variable from holding 7, and a reader meeting if (weather == 2) has to remember what 2 stood for. You could store it in a string, and then "suny" compiles and is wrong.
An enum class is a type whose values are exactly the names you list.
#include <iostream>
#include <string>
enum class Weather { Sunny, Cloudy, Rainy };
std::string name_of(Weather weather) {
switch (weather) {
case Weather::Sunny:
return "sunny";
case Weather::Cloudy:
return "cloudy";
case Weather::Rainy:
return "rainy";
}
return "unknown";
}
int main() {
Weather today = Weather::Cloudy;
Weather tomorrow = Weather::Rainy;
std::cout << "Today: " << name_of(today) << "\n";
if (tomorrow == Weather::Rainy) {
std::cout << "Take an umbrella tomorrow\n";
}
today = tomorrow;
if (today != Weather::Cloudy) {
std::cout << "Now it is " << name_of(today) << "\n";
}
}
Today: cloudy
Take an umbrella tomorrow
Now it is rainy
enum class Weather { Sunny, Cloudy, Rainy }; defines the type Weather and its three values. Like a struct definition, it goes above the functions that use it and ends with a semicolon.
A value is written with the type's name in front: Weather::Sunny. These are the same two colons as in std::cout, and they mean "the Sunny that belongs to Weather". Sunny on its own is a name the compiler does not know.
A Weather is not a number and not text.
Weather w = 1;does not compile, and neither doesWeather w = "Sunny";. The only values are the three that are listed, so aWeathervariable can never hold nonsense.- Two values can be compared with
==and!=. AWeathercan be assigned, passed to a function, returned, and stored in a vector or a struct. std::cout << today;does not compile. The names exist for the compiler, and the running program has no text for them.
That is why the program has the function name_of. It takes a Weather and returns the text to show. Its body is a switch with one case for every value. Each case returns, which leaves the function at once, so no break is needed.
The return "unknown"; after the switch is never reached today. It is there so that the function still returns a string if someone later adds a value to Weather and forgets to add its case.
Structs inside structs
A member can have any type, and that includes a struct you defined yourself, a vector, and an enum class.
#include <iostream>
#include <string>
#include <vector>
struct Spot {
int x;
int y;
};
struct Disc {
Spot centre;
int radius = 10;
};
struct Playlist {
std::string title;
std::vector<int> seconds;
};
int main() {
Disc coin{{200, 150}, 40};
coin.centre.x = coin.centre.x + 20;
std::cout << coin.centre.x << " " << coin.centre.y << " " << coin.radius << "\n";
Playlist morning{"Morning", {185, 240}};
morning.seconds.push_back(210);
std::cout << morning.title << ": " << morning.seconds.size() << " songs\n";
std::cout << "Second song: " << morning.seconds[1] << " seconds\n";
}
220 150 40
Morning: 3 songs
Second song: 240 seconds
A Disc has a Spot for its centre. Spot has to be defined above Disc, because the compiler reads from the top and must know what a Spot is when it meets the member.
In Disc coin{{200, 150}, 40}; the outer braces are the Disc and the inner braces are its first member, the Spot.
Read a chain of dots from left to right, and ask at each step what type you have.
| Expression | Its type |
|---|---|
coin |
Disc |
coin.centre |
Spot |
coin.centre.x |
int |
morning.seconds |
std::vector<int> |
morning.seconds[1] |
int |
morning.seconds is a vector like any other. Everything a vector can do is written after it: push_back, size(), an index in square brackets.
With members like these, one struct can hold everything a program has to remember. Everything about a quiz, for instance, can be one struct: a vector of answers given so far, the number of the current question, and whether time has run out. Such a struct is called the program's state. A function that takes the state by reference can change any part of it, and the caller still passes one argument.
Are two structs equal?
You can copy a struct with =. You cannot compare two of them with ==.
#include <iostream>
struct Spot {
int x;
int y;
};
int main() {
Spot a{3, 4};
Spot b{3, 4};
if (a == b) {
std::cout << "the same\n";
}
}
This does not compile. The message says, in one wording or another, that there is no == for two Spots.
The compiler does not guess what "equal" means for a type you made. For a Spot the meaning is clear enough. For a Player it is not: are two players equal when they have the same name, or only when the lives and the score agree as well? So you say what you mean, in a function.
#include <iostream>
struct Spot {
int x;
int y;
};
struct Disc {
Spot centre;
int radius = 10;
};
bool same_spot(const Spot& a, const Spot& b) {
return a.x == b.x && a.y == b.y;
}
bool same_disc(const Disc& a, const Disc& b) {
return same_spot(a.centre, b.centre) && a.radius == b.radius;
}
int main() {
Spot a{3, 4};
Spot b{3, 4};
Spot c{3, 9};
std::cout << same_spot(a, b) << "\n";
std::cout << same_spot(a, c) << "\n";
Disc coin{{200, 150}, 40};
Disc button{{200, 150}, 12};
std::cout << same_disc(coin, button) << "\n";
}
1
0
0
same_spot compares member with member. The members are ints, and == is defined for those. Two spots are the same only if every member agrees, so the comparisons are joined with &&. With || in its place, a and c would count as the same, because their x members agree.
Inside such a function, == can be used on any member whose type has it: numbers, bool, char, strings, and the values of an enum class. A member that is itself a struct is compared by calling its own function, as same_disc calls same_spot for the two centres. A member that is a vector is compared one element at a time. First the two sizes must be equal. Then a loop goes over the positions and returns false at the first one where the elements differ.
The same holds for < and for printing with <<: neither exists for a new type until somebody writes it. Part IV of the course shows how to give your own type an == of its own. Until then, a function with a clear name does the job.