Lab 7: Structs & Overloading

Custom Data Types

In this lab, we will learn about C++ structs (or structures) and how to use operator overloading to make structs more powerful and intuitive.

C++ structs provide a flexible way to group data, while operator overloading allows you to define custom behaviors for common operations. This lab will cover the essentials of structs, overloading operators, and advanced topics like serialization.

§1 Structs #

A struct in C++ is a way to group different variables under a single name, much like a class. Structs are often used to represent records or objects with multiple data members.

A struct declaration has the following format:

struct NAME {
  MEMBER;
};

In C++, the key difference between structs and classes is that members of a struct are public by default, whereas members of a class are private by default. We will explore this further when we learn about classes.

Basic Syntax and usage of Struct:

#include <iostream>

struct Book {  // define a struct and its members
  std::string title;
  std::string author;
  int pages;
};

int main() {
  Book myBook;  // initialize the struct and assign values to member variables
  myBook.title = "C++ Primer";
  myBook.author = "Stanley B. Lippman";
  myBook.pages = 976;

  // access struct variables
  std::cout << "Title: " << myBook.title << std::endl;
  std::cout << "Author: " << myBook.author << std::endl;
  std::cout << "Pages: " << myBook.pages << std::endl;
}

Here the struct keyword is used to define a new data type called Book, allowing us to create a Book struct and directly access its member fields, which are initialized using the dot (.) operator (myBook.title).

§1.1 Initializing Structs #

Structs can be initialized in a few ways:

Example: One line struct initialization with {} syntax

#include <iostream>
using namespace std;

struct Book {
  string title;
  string author;
  int pages;
};

int main() {
  // define struct variable and values in one line,
  Book myBook = {"C++ Primer", "Stanley B. Lippman", 976};

  cout << "Title: " << myBook.title << endl;
  cout << "Author: " << myBook.author << endl;
  cout << "Pages: " << myBook.pages << endl;
}

The curly brace syntax assigns variables in the same order as they are defined in the struct.

You do not have to assign all struct variables at once: Book myBook = {"C++ Primer"}; would still compile and run as valid code, all remaining variables will be initialized to the default value.

§1.2 Structs as Function Return Types #

A struct can be used as a return type for a function, which is useful when you need to return multiple related values.

Example: Returning a Struct from a Function

#include <iostream>
using namespace std;

struct Point {
  int x, y;
};

// Function that returns a struct
Point createPoint(int x, int y) {
  Point p;
  p.x = x;
  p.y = y;
  return p;
}

int main() {
  Point p = createPoint(5, 10);

  cout << "Point: (" << p.x << ", " << p.y << ")" << endl;  // Prints: Point: (5, 10)
}

In this example, the createPoint function returns a Point struct, allowing multiple values to be returned from the function.

§1.3 Passing Structs by Reference #

When you pass a struct to a function or as a return value, passing it by value (which invokes a copy) can be inefficient, especially for large structs. Instead, you can pass the struct by reference to avoid copying and improve performance.

Example: Passing Struct by Reference

#include <iostream>
using namespace std;

struct Person {
  string name;
  int age;
};

// Function to modify a Person struct
void setAge(Person& p, int newAge) {
  p.age = newAge; // Modifying the original struct
}

int main() {
  Person person = {"Alice", 25};
  cout << "Before: " << person.age << endl;  // Prints: Before: 25
  setAge(person, 30); // Pass by reference
  cout << "After: " << person.age << endl;  // Prints: After: 30
}

Passing by reference avoids copying the struct, making the function call more efficient.

§1.4 Structs and Dynamic Memory Allocation #

For advanced use cases, you can dynamically allocate memory for structs using new and delete. This is often needed when the lifetime of the struct must be managed manually, or for creating more complex data structures.

Accessing member variables of a struct pointer requires the arrow (->) operator: structPointer->var

Example: Dynamic Memory Allocation for Struct

#include <iostream>
using namespace std;

struct Node {
  int data;
  Node* next;
};

int main() {
  // Dynamically allocate a Node struct object
  Node* head = new Node;
  // access struct pointer members, notice the '->' syntax instead of using a dot '.'
  head->data = 10;
  head->next = nullptr;

  cout << "Node data: " << head->data << endl;  // Prints: Node data: 10

  // Free the allocated memory
  delete head;
}

The keyword new is used to allocate memory for a struct on the heap, while delete is used to free that memory once it is no longer needed, preventing memory leaks. To access a member of the struct through a pointer, the -> operator is used, which both dereferences the pointer and accesses the specified member.

§1.5 Structs Inside Structs (Nested Structs) #

You can nest structs inside of other structs to represent more complex data structures.

Example: Nested Structs

#include <iostream>
using namespace std;

struct Address {
  string city;
  string state;
  int zipCode;
};

struct Person {
  string name;
  Address address; // Nested struct, `Address` must be declared before use here
};

int main() {
  // using curly brace syntax, the nested struct can be defined inside the struct
  Person p = {"John", {"New York", "NY", 10001}};

  cout << p.name << " lives in " << p.address.city << ", " << p.address.state << endl;
  // Prints: John lives in New York, NY
}

The Person struct contains an Address struct as a member. This allows you to model more complex entities, where each entity consists of multiple related structures.

§1.6 Structs and std::tuple or std::pair #

For simpler use cases where you want to return or group a small number of values, consider using std::pair or std::tuple from the C++ Standard Library. These provide a quick way to group variables without needing to define a full struct.

Example: Using std::pair to return two variables instead of defining a custom struct

#include <iostream>
#include <utility> // For std::pair
using namespace std;

int main() {
  pair<string, int> book = {"C++ Primer", 976};

  cout << "Title: " << book.first << ", Pages: " << book.second << endl;
  // Prints: Title: C++ Primer, Pages: 976
}

Pairs are limited to only two items, however tuples can hold an arbitrary number of items.

Warning

To use tuples, you must compile your code with a standard library newer than C++11: g++ --std=c++23 -o test main.cpp

I always use the --std=c++23 flag when compiling submitted code, so you can freely use new C++ features.

Example: Using std::tuple to return several variables instead of defining a struct

#include <iostream>
#include <tuple>
using namespace std;

int main() {
  // define and populate a two item tuple
  tuple<string,int> two_item = make_tuple("C++ Primer", 976);
  // define and populate a four item tuple
  tuple<string,int,float,string> four_item = make_tuple("Moby Dick", 635, 12.5, "Fiction");
  // alternative shorthand tuple declaration:
  //tuple<string,int,float,string> four_item("Moby Dick", 635, 12.5, "Fiction");

  // access tuple members
  cout << "Title: " << get<0>(two_item) << ", Pages: " << get<1>(two_item) << endl;

  cout << "Title: " << get<0>(four_item) << ", Pages: " << get<1>(four_item);
  cout << ", Cost: " << get<2>(four_item) << ", Genre: " << get<3>(four_item) << endl;
}

std::pair and std::tuple are great alternatives to structs for simple use cases. They are particularly useful when you need to return multiple values from a function without needing to declare a dedicated struct.

§1.7 Serialization of Structs #

Serialization is the process of converting an object or data structure into a format that can be easily stored or transmitted (e.g., to a file or over a network) and later reconstructed. In C++, serializing a struct involves writing its member variables to a file or stream and then reading them back when deserializing.

There are different ways to serialize a struct, including binary or text-based serialization. Binary serialization is faster and more compact, while text-based serialization (e.g., JSON or XML) is more human-readable. We will cover text-based serialization since that is easier to work with and debug.

Text-Based Serialization Example (JSON-like)

#include <iostream>
#include <fstream>
#include <sstream>
using namespace std;

// Define struct
struct Book {
  string title;
  string author;
  int pages;
};

// Serialize Book struct to text file
void serializeToText(const Book& book, const string& filename) {
  ofstream file(filename);
  if (file.is_open()) {
    file << book.title << "," << book.author << "," << book.pages << endl;
    file.close();
  } else {
    cout << "Could not open file for writing!" << endl;
  }
}

// Deserialize Book struct from text file
Book deserializeFromText(const string& filename) {
  Book book;
  ifstream file(filename);
  if (file.is_open()) {
    getline(file, book.title, ',');
    getline(file, book.author, ',');
    string item; //temporary string for getline to store data in
    getline(file, item, ',');
    book.pages = stoi(item); // Convert string to integer
    file.close();
  } else {
    cout << "Could not open file for reading!" << endl;
  }
  return book;
}

int main() {
  Book book1 = {"C++ Primer", "Stanley B. Lippman", 976};

  // Serialize the book to text
  serializeToText(book1, "book.txt");

  // Deserialize the book from text
  Book book2 = deserializeFromText("book.txt");

  cout << "Deserialized Book:\n";
  cout << "Title: " << book2.title;
  cout << "Author: " << book2.author;
  cout << "Pages: " << book2.pages << endl;
}

Output:

Deserialized Book:
Title: C++ Primer
Author: Stanley B. Lippman
Pages: 976

This example writes the Book struct to a text file in a human-readable format, then the deserializeFromText function reads the file and parses the text back into the struct.

This approach makes the data easy to inspect and modify by humans, but it is slower and bulkier compared to binary serialization.

§2 Operator Overloading #

Operator overloading allows you to redefine the behavior of operators for user-defined types (such as structs or classes). This can make the code more readable and intuitive by allowing operators like +, -, ==, and more to work with your structs.

When overloading as a member function, the first operand of the operator must be an instance of the struct. The general syntax is:

ReturnType operatorOP(ArgumentList);

§2.1 Overloading Arithmetic Operators for Structs #

You can also overload arithmetic operators like +, -, *, and more to allow operations between structs.

Example: Overloading the + Operator
Suppose we have a struct representing a Point in 2D space, and we want to add two Point objects.

#include <iostream>
using namespace std;

struct Point {
  int x, y;

  // Overload the + operator for Point
  // define the operator overload within the struct definition
  // the + operator will return a new 'Point' struct
  // we use 'const' to ensure we don't accidentally change the data on our target structs
  Point operator+(const Point& other) const {
    Point result;
    result.x = this->x + other.x;
    // `this->x` refers to the 'x' variable on this struct instance
    // `other.x` refers to the 'x' variable on the struct being added to this struct
    result.y = this->y + other.y;
    return result;
  }
};

int main() {
  Point p1 = {3, 4};
  Point p2 = {1, 2};

  Point sum = p1 + p2; // Using overloaded + operator

  cout << "Sum of points: (" << sum.x << ", " << sum.y << ")" << endl;
  // Sum of points: (4, 6)
}

The same procedure can be used to overload the -, /, and * operators.

§2.2 Overloading Comparison Operators #

You can also overload comparison operators such as ==, <, >, etc. For example, let’s overload the == operator for a Book struct to compare two books by their title and author.

Example: Overloading the == Operator

#include <iostream>
using namespace std;

struct Book {
  string title;
  string author;
  int pages;

  // Overload the == operator
  bool operator==(const Book& other) const {
      // we can define custom logic to decide if two structs are equal
      return (title == other.title && author == other.author);
  }
};

int main() {
  Book book1 = {"C++ Primer", "Stanley B. Lippman", 976};
  Book book2 = {"C++ Primer", "Stanley B. Lippman", 976};
  Book book3 = {"Effective C++", "Scott Meyers", 320};

  if (book1 == book2) {
    cout << "book1 and book2 are the same!" << endl;
  }

  if (book1 != book3) {
    cout << "book1 and book3 are different!" << endl;
  }
}

§3 Questions #

  1. What is the primary purpose of using structs in C++?
  1. Why would you want to pass a struct by reference rather than by value?
  1. What operator is used to access members of a struct member variable when the struct is stored in a pointer? Provide a 1-line code sample.
  1. When would you want to use a struct (or tuple or pair) as a return type for a function?
  1. What is the purpose of using a nested struct?
  1. Provide an example situation where a nested struct could be useful. Provide a code sample to demonstrate.
  1. How do you declare a tuple that contains an int, a float, and a string, and also assign some values to it? provide code.
  1. What are some potential downsides of using a tuple over a struct? name at least two.
  1. In your own words, explain operator overloading in C++ and why is it useful.
  1. What is serialization?
  1. What are two reasons you would serialize a struct?