Classes and Objects

This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com.

This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Members, Constructors, and Member-Initializer Lists

#include <string>

class Person {
public:
    Person(std::string name, int age)
        : name_(std::move(name)), age_(age) {}   // member-initializer list -- initializes, doesn't assign

    const std::string& name() const { return name_; }
    int age() const { return age_; }

private:
    std::string name_;
    int age_;
};

Members are always initialized in declaration order, regardless of the order listed in the member-initializer list (a mismatch here is a common -Wreorder warning) — prefer using std::move for by-value parameters that are stored, exactly as name is above.

Special Member Functions and the Rule of Zero/Three/Five

Six functions are "special": default constructor, destructor, copy constructor, copy-assignment, move constructor, move-assignment. Declaring any of the copy/move/destructor set changes which of the others the compiler still generates:

class RuleOfFive {
public:
    RuleOfFive() = default;
    ~RuleOfFive() { delete[] data_; }                                  // 1. destructor
    RuleOfFive(const RuleOfFive& other)                                 // 2. copy constructor
        : size_(other.size_), data_(new int[other.size_]) {
        std::copy(other.data_, other.data_ + size_, data_);
    }
    RuleOfFive& operator=(const RuleOfFive& other) {                    // 3. copy assignment
        if (this == &other) return *this;
        delete[] data_;
        size_ = other.size_;
        data_ = new int[size_];
        std::copy(other.data_, other.data_ + size_, data_);
        return *this;
    }
    RuleOfFive(RuleOfFive&& other) noexcept                              // 4. move constructor
        : size_(other.size_), data_(other.data_) {
        other.size_ = 0;
        other.data_ = nullptr;
    }
    RuleOfFive& operator=(RuleOfFive&& other) noexcept {                  // 5. move assignment
        if (this == &other) return *this;
        delete[] data_;
        size_ = other.size_;
        data_ = other.data_;
        other.size_ = 0;
        other.data_ = nullptr;
        return *this;
    }

private:
    std::size_t size_ = 0;
    int* data_ = nullptr;
};

Declaring a destructor that manages a raw resource, as above, is exactly when the rule of five applies — if RuleOfFive didn’t declare a destructor at all, none of the other four would need declaring either. The rule of zero is the preferred outcome: own resources through std::unique_ptr/std::vector/std::string instead of raw pointers, so none of the six special members need writing — the compiler-generated ones already do the right thing by delegating to the members:

#include <vector>
#include <memory>

class RuleOfZero {
public:
    // no destructor, no copy/move ctors/assignment declared -- the compiler-generated ones
    // correctly copy/move buffer_ and owned_, because vector/unique_ptr already implement rule of five themselves
private:
    std::vector<int> buffer_;
    std::unique_ptr<int> owned_;
};

Which of the six special members the compiler implicitly declares (and whether as defaulted or deleted) follows a fixed decision procedure driven by what a class explicitly declares itself:

flowchart TD A["Class declares nothing special"] --> B["All six generated: default ctor,
destructor, copy ctor/assign,
move ctor/assign"] C["Class declares a destructor
or any copy/move member"] --> D{"Which one(s)?"} D -->|"destructor only"| E["Copy ctor/assign still generated
(deprecated), move NOT generated
-- falls back to copy"] D -->|"copy ctor or copy assign"| F["Move ctor/assign NOT generated
-- falls back to copy"] D -->|"move ctor or move assign"| G["Copy ctor/assign implicitly DELETED
-- copying becomes a compile error"] H["Member/base is not copyable
(e.g. holds unique_ptr)"] --> I["Copy ctor/assign implicitly DELETED"]

explicit

Covered in depth in Operator Overloading and Conversions; on a constructor it blocks that constructor from being used for an implicit conversion:

class Meters {
public:
    explicit Meters(double value) : value_(value) {}
    double value_;
};

void printDistance(Meters m);
// printDistance(5.0);       // error: no implicit double -> Meters conversion
printDistance(Meters(5.0));   // fine: explicit construction

static Members

class Counter {
public:
    Counter() { ++instanceCount_; }
    ~Counter() { --instanceCount_; }
    static int instanceCount() { return instanceCount_; }   // static member function: no "this", callable
                                                              // without an instance
private:
    static inline int instanceCount_ = 0;   // inline (C++17): definable in the class body, no separate .cpp needed
};

friend

A friend grants another function or class access to this class’s private members — an escape hatch to use sparingly, since it breaks encapsulation between the two types:

class Box {
public:
    explicit Box(int volume) : volume_(volume) {}
    friend bool sameVolume(const Box& a, const Box& b);   // can reach a.volume_/b.volume_ directly

private:
    int volume_;
};

bool sameVolume(const Box& a, const Box& b) { return a.volume_ == b.volume_; }

const/mutable Correctness and this

A member function marked const promises not to modify the object (the compiler enforces it) and can be called on const objects/references; mutable opts a specific member out of that promise, for state that is an implementation detail rather than part of the object’s logical value (a cache, a mutex):

#include <string>

class Circle {
public:
    explicit Circle(double radius) : radius_(radius) {}

    double area() const {                    // const: does not modify *this
        if (!areaCached_) {
            cachedArea_ = 3.14159265358979 * radius_ * radius_;   // allowed: cachedArea_/areaCached_ are mutable
            areaCached_ = true;
        }
        return cachedArea_;
    }

    Circle& scale(double factor) {             // non-const: modifies *this, returns *this for chaining
        radius_ *= factor;
        areaCached_ = false;
        return *this;                           // "this" is Circle* here; *this dereferences it
    }

private:
    double radius_;
    mutable double cachedArea_ = 0.0;
    mutable bool areaCached_ = false;
};

See Also