Contents

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

1. What goes wrong with three parallel vectors instead of one vector of structs?

Not quite. Broadly the same memory either way. The problem is correctness, not size.

Yes. Position is the only thing linking them, so any operation that reorders one and not the others silently scrambles your data.

Not quite. You can sort each of them, which is exactly the trap. Sorting one leaves the others behind.

2. Contact c; what is in c.age?

Not quite. They do not, here or anywhere. Chapter 3 made the same point about a plain local int.

Yes. The string field initialises itself and the int does not, which makes the mixed behaviour especially easy to miss. Contact c{}; zeroes it.

Not quite. It compiles happily. That is the problem.

3. Why should a comparison function take const Contact& rather than Contact?

Not quite. It accepts them and the program works. It is just slower than it needs to be.

Yes. Chapter 12’s rule applied where it bites hardest: a function called repeatedly in a loop you did not write.

Not quite. It can, and assigning one copies it. Passing by value is legal and simply wasteful here.

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 Contact with name, phone and age
  • std::vector<Contact> book; in main
  • void print_contact(const Contact&)
  • Contact read_contact(), which prompts for three fields and returns one
  • bool by_name(const Contact&, const Contact&)
  • a while loop around a menu, with an if chain 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.