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;
};
- You declare the struct with the
structkeyword, followed by the name of your struct. - You can then declare the “members” of the struct inside of it.
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:
- Declare a struct variable and manually assign the variable values on separate lines (shown in previous example above).
- Declare the struct variable and values using the curly brace syntax
{}. - Define and use a constructor for the struct (we will cover this when we learn about classes).
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
}
- A
paircan be initialized using curly brace syntax{}, like any other struct. - You access the items in the
pairthrough its members:.first, and.second.
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;
}
- Create a
tuple: the number of items, and their types:tuple<string,int> two_item. - Initialize the
tuplevalues withmake_tuple(value1, value2). - Access the tuple member values with
get<number>(tuple_name), e.g.get<0>(two_item)for the first item in the tuple.
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);
ReturnTypeis the type of the value returned (often the same type as the struct).operatoris a special keyword.OPis the operator being overloaded (e.g.,+,-,==, etc.).ArgumentListis the list of parameters.
§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;
}
}
- The
operator==is overloaded to compare thetitleandauthormembers of twoBookobjects. - In this example,
book1andbook2have the same title and author, so they are considered equal, whilebook1andbook3are different. The==operator is used for both==and!=operations.
§3 Questions #
- What is the primary purpose of using structs in C++?
- Why would you want to pass a struct by reference rather than by value?
- 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.
- When would you want to use a struct (or tuple or pair) as a return type for a function?
- What is the purpose of using a nested struct?
- Provide an example situation where a nested struct could be useful. Provide a code sample to demonstrate.
- How do you declare a tuple that contains an
int, afloat, and astring, and also assign some values to it? provide code.
- What are some potential downsides of using a
tupleover astruct? name at least two.
- In your own words, explain operator overloading in C++ and why is it useful.
- What is serialization?
- What are two reasons you would serialize a struct?