Classes and Structs

Classes and Structs

Bundle data with behavior, maintain invariants, and design small types cleanly.

Classes and Structs

Building a type

class BankAccount {
public:
    explicit BankAccount(double balance) : balance_{balance} {}

    void deposit(double amount) {
        balance_ += amount;
    }

    double balance() const {
        return balance_;
    }

private:
    double balance_{};
};

Why classes matter

They let you keep related data and rules together.

That matters because valid state is easier to maintain when the type itself enforces the rules.

Key habits

Add behavior next to the data

class BankAccount {
public:
    explicit BankAccount(double balance) : balance_{balance} {}

    void deposit(double amount) {
        if (amount > 0.0) {
            balance_ += amount;
        }
    }

    bool withdraw(double amount) {
        if (amount > balance_) {
            return false;
        }
        balance_ -= amount;
        return true;
    }

    double balance() const {
        return balance_;
    }

private:
    double balance_{};
};

This is better than exposing a public balance field and expecting every caller to remember the same validity checks.

Larger worked example

class TemperatureRange {
public:
    TemperatureRange(double low, double high) : low_{low}, high_{high} {
        if (high_ < low_) {
            std::swap(low_, high_);
        }
    }

    bool contains(double value) const {
        return value >= low_ && value <= high_;
    }

    double width() const {
        return high_ - low_;
    }

private:
    double low_{};
    double high_{};
};

The constructor establishes a usable invariant immediately, and the member functions keep the logic close to the stored data.

struct vs class

Use struct for simple aggregates with obvious fields and little behavior. Use class when you need stronger encapsulation or invariants.

struct Point {
    int x{};
    int y{};
};

This is a good struct because it is just a small value object with obvious fields.

Rule of zero

If your members already manage themselves, let the compiler generate constructors, destructors, and assignment operators.

struct UserProfile {
    std::string name;
    std::vector<int> scores;
};

If your members already manage memory and cleanup safely, avoid writing custom destructor, copy, or move operations unless you genuinely need special behavior.

Rule of five

When a type directly owns a raw resource, you typically need to define (or delete) all five special member functions.

class Buffer {
public:
    explicit Buffer(std::size_t size)
        : data_{new int[size]}, size_{size} {}

    ~Buffer() { delete[] data_; }

    Buffer(const Buffer& other)
        : data_{new int[other.size_]}, size_{other.size_} {
        std::copy(other.data_, other.data_ + size_, data_);
    }

    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            auto* copy = new int[other.size_];
            std::copy(other.data_, other.data_ + other.size_, copy);
            delete[] data_;
            data_ = copy;
            size_ = other.size_;
        }
        return *this;
    }

    Buffer(Buffer&& other) noexcept
        : data_{other.data_}, size_{other.size_} {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

private:
    int* data_{};
    std::size_t size_{};
};

In practice, prefer wrapping raw resources in a smart pointer or standard container so you can rely on the rule of zero instead.

Modern value types

For many domain objects, a small value type with clear fields, comparisons, and no manual resource management is a better design than a large inheritance hierarchy.

struct Point {
    int x{};
    int y{};
    auto operator<=>(const Point&) const = default;
};

This is a good value type: easy to copy, easy to compare, and free of manual lifetime management.

Constructor guidance

Small design checklist

Practical rule of thumb

Example in practice

struct Point {
    int x{};
    int y{};
};

int main() {
    Point p{3, 4};
    return p.x + p.y;
}

Try this variation

Add a member function that keeps the invariants next to the data. That is the simplest way to move from passive structs to useful types.

struct Point {
    int x{};
    int y{};

    int magnitude_squared() const {
        return x * x + y * y;
    }
};