Chapter 14
struct: Things That Belong Together
Grouping fields into one type of your own.
Say you’re keeping a list of people, each with a name, a phone number and an age. With what you have, that’s three vectors:
std::vector<std::string> names;
std::vector<std::string> phones;
std::vector<int> ages;
It works, right up until it doesn’t. names[2] and phones[2] are the same person
only by convention, since nothing enforces it, and the day you sort names and forget
to sort the other two in the same order, everyone gets a stranger’s phone number.
That bug is silent, and there is no version of it the compiler can catch.
The problem is that the language doesn’t know these three things are related. So tell it.
Making a type
struct Contact {
std::string name;
std::string phone;
int age;
};
That’s a new type called Contact, made of three existing ones. Now
std::vector<Contact> is a list of whole people, and there is nothing left to
keep in step.
Note the semicolon after the closing brace. It is not optional and it is not like a function:
error: expected ';' after struct
Every C++ programmer has done this. The error is at least clear about it, which is more than you can say for some.
Getting at the fields
The dot again, the same dot as full.size() in chapter 4, and this time you can
see exactly what it means, because you wrote the fields yourself:
Contact ada;
ada.name = "Ada";
ada.phone = "555-0100";
ada.age = 36;
std::cout << ada.name << " is " << ada.age << '\n';
Ada is 36
ada is one variable holding three values. ada.name is an ordinary
std::string, so everything chapter 4 taught still applies, so ada.name.size()
works and reads about as you’d expect.
Filling fields one line at a time gets old. The brace form does it in one:
Contact grace{"Grace", "555-0199", 29};
Values go in the order the fields were declared, which is a real cost of the
short form: get the order wrong between two std::string fields and the compiler
cannot possibly tell. Phone numbers in the name column, no error.
Structs are values
A Contact behaves like an int does. Assigning copies it:
Contact copy = grace;
copy.name = "Changed";
std::cout << grace.name << '\n';
Grace
No sharing, no surprises. And because it’s a value, everything chapter 12 said
applies unchanged, which means a function that only reads a Contact should take
it by const&:
void print_contact(const Contact& c) {
std::cout << c.name << " " << c.phone << " (" << c.age << ")\n";
}
Three strings and an int is already worth not copying, and it will only grow. Functions that make a contact return one by value:
Contact make_contact(const std::string& name,
const std::string& phone, int age) {
Contact c;
c.name = name;
c.phone = phone;
c.age = age;
return c;
}
const& in, plain value out. The rule from chapter 12, now with a type you own.
The payoff: sorting by whatever you like
Chapter 13’s third argument to sort looked like a lot of ceremony for putting
numbers backwards. Here is what it was for.
bool by_name(const Contact& a, const Contact& b) {
return a.name < b.name;
}
bool by_age(const Contact& a, const Contact& b) {
return a.age < b.age;
}
std::sort(book.begin(), book.end(), by_name);
std::sort(book.begin(), book.end(), by_age);
-- by name
Ada 555-0100 (36)
Alan 555-0142 (41)
Grace 555-0199 (29)
-- by age
Grace 555-0199 (29)
Ada 555-0100 (36)
Alan 555-0142 (41)
Two orderings of the same list, and the difference between them is one word.
std::sort has no idea what a Contact is; it just asks your function which of
two goes first, and your function knows because you wrote it.
Notice the whole record moves. Sorting by age carries each person’s name and phone along with them, because they are one value and always were. That is the bug from the top of the chapter, gone. Not caught, not warned about, just impossible.
Also notice both comparison functions take const&. sort calls them a great
many times, and copying a contact on every comparison would be a lot of copying
for nothing.
Structs inside structs
A struct’s fields can be any type, including another struct:
struct Address {
std::string street;
std::string city;
};
struct Contact {
std::string name;
Address home;
};
Contact ada{"Ada", {"12 Mill Lane", "London"}};
std::cout << ada.name << " of " << ada.home.city << '\n';
ada.home.city = "Bath";
Ada of London
Ada of Bath
Dots chain: ada.home.city is the city of the home of Ada. The braces nest the
same way, with the inner set filling the inner struct.
This is how programs actually grow. You don’t add nine fields to Contact; you
notice that four of them are really an address, give that a name, and Contact
goes back to being short. A type whose fields you can hold in your head is worth
more than one that saves a line.
Exercise 1 · Design a small one
Write a struct Book with a title, an author and a page count. Make three of
them: one with the dot form, one with braces, one from a make_book function.
Put them in a std::vector<Book>, print them with a
void print_book(const Book&), then sort by page count and print again.
Then do it wrong on purpose: swap the title and author in one of the braced ones. Confirm the compiler says nothing at all, and that the only symptom is wrong output.
Check yourself
Project
The contact book
Roughly 60 minutes
A menu-driven contact book. It is the biggest program in the book so far, and chapter 20 comes back and splits it across files, so build it tidily.
1) Add 2) List 3) Sort by name 4) Find 5) Quit
Choice: 1
Name: Ada
Phone: 555-0100
Age: 36
Added.What it needs:
struct Contactwith name, phone and agestd::vector<Contact> book;inmainvoid print_contact(const Contact&)Contact read_contact(), which prompts for three fields and returns onebool by_name(const Contact&, const Contact&)- a
whileloop around a menu, with anifchain on the choice
Two things you already know that this needs.
Reading a name after reading a number needs std::cin.ignore(), chapter 5’s
leftover-newline trap, and this is the first program big enough for it to bite
twice in one run.
Adding a contact means book.push_back(read_contact());, a function call
straight into another function call, no temporary variable. Chapter 9 said a call
returning a Contact is a Contact wherever one fits.
Stretch one, find by name. Ask for a name, loop the book, print the match or
“not found”. You’ll notice you want to return the contact you found, and that a
function returning Contact has no honest way to say “there wasn’t one”.
Returning an empty contact is a lie you’d have to remember. Chapter 17 gives
you the right answer, so leave the ugly version in and come back to it.
Stretch two, sort by whatever. Add a menu option for sorting by age, then
one for reverse alphabetical. Three comparison functions, one sort call each,
and no other change to the program.