A class defines a type with data and operations. Its most useful job is often enforcing an invariant, a condition that must hold whenever callers can observe the object. A reading list might require a nonempty name, nonempty entries and a maximum number of entries. Private members prevent callers from bypassing those rules.
struct and class offer the same language facilities; their default member and base-class access differs. A struct defaults to public access, while a class defaults to private access. Use a simple struct for a record whose fields can vary independently. Use a class when operations must preserve relationships between fields.
Establish valid state at construction
Save as classes.cpp:
#include <cstddef>
#include <exception>
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
class ReadingList {
public:
explicit ReadingList(std::string name) : name_(std::move(name)) {
if (name_.empty()) {
throw std::invalid_argument("List name must not be empty");
}
}
void add(std::string title) {
if (title.empty()) {
throw std::invalid_argument("Title must not be empty");
}
if (titles_.size() >= max_titles) {
throw std::length_error("Reading list is full");
}
titles_.push_back(std::move(title));
}
void print(std::ostream& output) const {
output << name_ << ": " << titles_.size() << " titles\n";
for (const auto& title : titles_) {
output << "- " << title << '\n';
}
}
private:
static constexpr std::size_t max_titles{1000};
std::string name_;
std::vector<std::string> titles_;
};
int main() {
try {
ReadingList original{"Weekend"};
original.add("C++ Basics");
auto expanded = original;
expanded.add("Testing");
original.print(std::cout);
expanded.print(std::cout);
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
clang++ -std=c++20 -Wall -Wextra -Wpedantic -g classes.cpp -o classes
./classes
Expected output:
Weekend: 1 titles
- C++ Basics
Weekend: 2 titles
- C++ Basics
- Testing
The constructor initializes the name through a member initializer list, the section after its colon. explicit prevents this one-argument constructor from silently converting arbitrary strings into reading lists. A const member function promises not to modify ordinary data members through this, the pointer to the current object.
std::move allows the string members to take resources from the by-value parameters; the next lesson examines that operation. Copying original copies its string and vector, producing independent values. Adding to expanded leaves the original's entries unchanged.
RAII connects lifetime to cleanup
RAII, Resource Acquisition Is Initialization, means placing a resource inside an object whose lifetime controls cleanup. A vector owns allocated element storage; a file stream owns an open file; a lock guard owns responsibility for releasing a lock. When the owner is destroyed, its destructor performs the corresponding cleanup.
Local objects are normally destroyed in reverse construction order when their scope exits, including during exception unwinding toward a matching handler. Members are destroyed in reverse declaration order. They are initialized in declaration order too, regardless of how you order the constructor's initializer list. Declare dependent members after the members they depend on.
If this constructor rejects an empty name, no usable ReadingList is produced. Already constructed members are destroyed automatically, even though the destructor of the incomplete enclosing object is not called. This is why composing owning members is safer than acquiring untracked raw resources partway through construction.
The throw expressions report failure to the catch in main; error-handling policy is covered in lesson ten. RAII is not a guarantee of cleanup after abrupt process termination, operating-system failure or every possible termination path. Its useful guarantee concerns normal object destruction.
Prefer the rule of zero
The rule of zero means letting resource-owning members implement destruction, copying and moving for your class. This example needs no custom destructor or copy operations. Writing a destructor that manually deletes a vector's storage would violate ownership and cause serious errors.
If you must implement a low-level resource wrapper, reason about all special member operations together. A user-declared destructor affects implicit move generation. Some resources should be move-only, while others cannot be moved at all. Avoid adding inheritance merely to reuse a few fields; composition keeps ownership easier to follow. Deleting a derived object through a base pointer also requires an appropriate virtual destructor, a concern absent from this value-based design.
Practice and solution guidance
- Reject duplicate titles. Guidance: search before insertion and specify whether duplication is an error or a harmless no-op; preserve the invariant on failure.
- Test constructing an empty-name list. Guidance: catch
std::invalid_argumentand confirm that no partially valid object escapes. - Add a rename operation. Guidance: validate the replacement before modifying the current name, then explain what state remains if validation fails.

