Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

C++ Constructors and Destructors - Complete Guide

Table of Contents

  1. Constructors
  2. Destructors
  3. The explicit Keyword
  4. Constructor Initializer Lists
  5. The this Pointer and Const Member Functions
  6. The mutable Keyword
  7. Copy constructor

1. Constructors

Constructors are special member functions that share the same name as their class.

Common Misconception

Many people believe constructors create objects, but this isn't accurate.

What Constructors Actually Do

Constructors are special functions designed to initialize an object immediately after it has been created. When an object is instantiated, memory is first allocated for it, and then the constructor is automatically invoked to set up the object's initial state—assigning values to member variables, allocating resources, or performing any other setup operations needed before the object is ready to use.

Key Points:

  • Object creation (memory allocation) happens first
  • Constructor invocation (initialization) happens immediately after
  • Constructors ensure objects start in a valid, well-defined state
  • They are called automatically—you don't invoke them manually

Object Lifetime Flow

1. Memory Allocation
2. Constructor Execution ← Initialization
3. Object Usage
4. Destructor Execution ← Cleanup
5. Memory Deallocation

Basic Code Example

#include <iostream>

class Foo {
    private:
        int member;
    public:
        /* Default Constructor */
        Foo() { 
            std::cout << "Foo() invoked\n"; 
        }
        
        /* Parameterized constructor */
        Foo(int a) {
            this->member = a;
            std::cout << "Foo(int a) invoked\n";
        }
        
        /* Destructor */
        ~Foo() {
            std::cout << "~Foo() invoked\n";
        }

        void print_obj() {
            std::cout << "Object Add: " << this << ": member : " << this->member << std::endl;
        }
};

int main(int argc, char* argv[]) {
    Foo obj1; // Default constructor invoke
    obj1.print_obj();
    
    Foo obj2(2); // Explicitly Parameterized constructor invoked
                 // Explicit conversion
    obj2.print_obj();
    
    Foo obj3 = 10; // Parameterized constructor will be invoked 
                   // Implicit type conversion from int to Foo Type 
    obj3.print_obj();
    
    return 0;
    // Destructors are called here automatically in reverse order: obj3, obj2, obj1
}

Output

➜  practice g++ -O0 -fno-elide-constructors constructor_example.cpp
➜  practice ./a.out       
Foo() invoked
Object Add: 0x16f64704c: member : 1
Foo(int a) invoked
Object Add: 0x16f647038: member : 2
Foo(int a) invoked
Object Add: 0x16f647034: member : 10
~Foo() invoked
~Foo() invoked
~Foo() invoked

Explanation

This example demonstrates three ways to create objects:

  1. Foo obj1; - Calls the default constructor (no parameters)
  2. Foo obj2(2); - Calls the parameterized constructor with explicit syntax
  3. Foo obj3 = 10; - Calls the parameterized constructor through implicit conversion from int to Foo

All three objects are destroyed at the end of main() when they go out of scope, invoking their destructors in reverse order of creation (obj3 → obj2 → obj1). This ensures that dependencies between objects are properly handled during cleanup.

↑ Back to Table of Contents


2. Destructors

Destructors are special member functions that have the same name as the class, but prefixed with a tilde (~).

What Destructors Actually Do

Destructors are special functions designed to clean up an object just before it is destroyed. When an object goes out of scope or is explicitly deleted, the destructor is automatically invoked to perform cleanup operations—releasing dynamically allocated memory, closing file handles, releasing locks, or performing any other necessary cleanup before the object's memory is deallocated.

Key Points:

  • Destructor invocation (cleanup) happens first
  • Object destruction (memory deallocation) happens immediately after
  • Destructors ensure proper resource cleanup and prevent memory leaks
  • They are called automatically when an object goes out of scope or is deleted
  • A class can have only one destructor (no overloading, no parameters)
  • Destructors are called in reverse order of object creation

Example

See the code example in the Constructors section above, which demonstrates both constructors and destructors working together.

↑ Back to Table of Contents


3. The explicit Keyword

Why Implicit Conversions Are Problematic

Implicit conversions can lead to several issues:

  1. Unintended Behavior - The compiler silently converts types, which may not be what you intended
  2. Harder to Debug - When something goes wrong, it's difficult to trace back to an implicit conversion
  3. Reduces Code Clarity - Other developers reading your code may not realize a conversion is happening
  4. Potential Performance Issues - Unnecessary temporary objects may be created
  5. Type Safety Loss - You lose the strict type checking that helps catch errors at compile time

Example of the Problem

class Foo {
    int member;
public:
    Foo(int a) { member = a; }
};

void process(Foo obj) {
    // Does something with Foo object
}

int main() {
    process(42);  // Compiles! But is this really what you meant?
                  // 42 is implicitly converted to Foo object
}

In the above code, you probably meant to pass a Foo object, but accidentally passed an int. The compiler doesn't complain—it just silently converts 42 to a Foo object. This can hide bugs!

Solution: The explicit Keyword

The explicit keyword prevents implicit conversions by forcing the programmer to explicitly construct objects.

When you mark a constructor as explicit, the compiler will only allow explicit construction and will reject implicit conversions.

Code Example with explicit

#include <iostream>

class Foo {
    private:
        int member;
    public:
        /* Default Constructor */
        explicit Foo() { 
            std::cout << "Foo() invoked\n"; 
        }
        
        /* Parameterized constructor marked as explicit */
        explicit Foo(int a) {
            this->member = a;
            std::cout << "Foo(int a) invoked\n";
        }
        
        ~Foo() {
            std::cout << "~Foo() invoked\n";
        }

        void print_obj() {
            std::cout << "Object Add: " << this << ": member : " << this->member << std::endl;
        }
};

int main(int argc, char* argv[]) {
    Foo obj1;      // ✓ OK: Default constructor invoked explicitly
    obj1.print_obj();
    
    Foo obj2(2);   // ✓ OK: Parameterized constructor invoked explicitly
    obj2.print_obj();
    
    // ✗ COMPILATION ERROR: Implicit conversion not allowed!
    // Foo obj3 = 10;  
    
    // ✓ OK: If you really want to convert, you must do it explicitly:
    // Foo obj3 = Foo(10);  // This would work
    // or
    // Foo obj3{10};        // This would also work
    
    return 0;
}

Benefits of Using explicit

1. Prevents Accidental Bugs

explicit Foo(int a);

void doSomething(Foo obj) { }

doSomething(42);        // ✗ Compilation error - catches the mistake!
doSomething(Foo(42));   // ✓ OK - you clearly meant to create a Foo

2. Makes Code More Readable

When someone reads Foo obj(10), it's crystal clear that a Foo object is being created. With Foo obj = 10, it's less obvious what's happening.

3. Enforces Type Safety

You maintain C++'s strong typing system. If you want a Foo object, you must explicitly create one—no shortcuts.

4. Reduces Unexpected Behavior

No surprise conversions means no surprise bugs. What you write is what you get.

Best Practice Rules

DO: Mark single-parameter constructors as explicit by default

class String {
public:
    explicit String(int size);  // Good!
};

DON'T: Allow implicit conversions unless you have a very good reason

class String {
public:
    String(int size);  // Dangerous! int could be silently converted to String
};

Comparison: With vs Without explicit

Without explicitWith explicit
Foo obj = 10; ✓ compilesFoo obj = 10; ✗ error
Foo obj(10); ✓ compilesFoo obj(10); ✓ compiles
Implicit conversions allowedOnly explicit conversions allowed
Can hide bugsCatches bugs at compile time
Less clear intentCrystal clear intent

↑ Back to Table of Contents


4. Constructor Initializer Lists

The Problem with Const Member Variables

Consider this problematic code:

#include <iostream>

class Foo {
    private:
        /* We have a member whose storage is const */
        const int member;
    public:
        /* Default Constructor */
        explicit Foo() { 
            std::cout << "Foo() invoked\n"; 
        }
        
        /* Parameterized constructor - THIS WILL NOT COMPILE! */
        explicit Foo(int a){
            this->member = a;  // ❌ ERROR: Cannot assign to const member!
            std::cout << "Foo(int a) invoked\n";
        }
        
        ~Foo() {
            std::cout << "~Foo() invoked\n";
        }

        void print_obj() {
            std::cout << "Object Add: " << this << ": member : " << this->member << std::endl;
        }
};

Why This Fails

The above code will not compile! The compiler will give an error like:

error: assignment of read-only member 'Foo::member'

The Problem: You cannot assign a value to a const member variable. Once a const variable is created, it cannot be changed.

When you write this->member = a; inside the constructor body, you're trying to assign to member after it has already been created. But member is const, so assignment is forbidden!

Understanding Object Creation Flow

To understand the solution, we need to understand what happens when an object is created:

Step-by-Step Object Creation:

1. Memory Allocation
   └─> Space for the object is allocated on stack/heap

2. Member Variable Construction (BEFORE constructor body)
   └─> All member variables are constructed/created
   └─> This happens BEFORE the constructor body executes
   └─> For const members, they MUST be initialized here!

3. Constructor Body Execution
   └─> The code inside { } of the constructor runs
   └─> At this point, all members already exist
   └─> You can only ASSIGN values here, not INITIALIZE

4. Object is Ready to Use

Key Insight: By the time the constructor body { } executes, all member variables have already been constructed. For const members, it's too late to initialize them—you can only initialize them during step 2, not during step 3.

The Solution: Member Initializer List

The member initializer list allows you to initialize member variables before the constructor body executes—exactly when they are being constructed.

Syntax

ClassName(parameters) : member1(value1), member2(value2) {
    // Constructor body
}

The part after : and before { is the initializer list.

Corrected Code Example

#include <iostream>

class Foo {
    private:
        const int member;  // const member variable
    public:
        /* Default Constructor with initializer list */
        explicit Foo() : member(0) {  // Initialize member to 0
            std::cout << "Foo() invoked\n"; 
        }
        
        /* Parameterized constructor with initializer list */
        explicit Foo(int a) : member(a) {  // Initialize member with 'a'
            std::cout << "Foo(int a) invoked\n";
        }
        
        ~Foo() {
            std::cout << "~Foo() invoked\n";
        }

        void print_obj() {
            std::cout << "Object Add: " << this << ": member : " << this->member << std::endl;
        }
};

int main(int argc, char* argv[]) {
    Foo obj1;      // Default constructor - member initialized to 0
    obj1.print_obj();
    
    Foo obj2(42);  // Parameterized constructor - member initialized to 42
    obj2.print_obj();
    
    return 0;
}

Output

Foo() invoked
Object Add: 0x16fdff04c: member : 0
Foo(int a) invoked
Object Add: 0x16fdff048: member : 42
~Foo() invoked
~Foo() invoked

How Initializer Lists Fix the Problem

What Happens with Initializer List:

Foo(int a) : member(a) {  // Initializer list
    // Constructor body
}

Step-by-Step Flow:

  1. Memory Allocation - Space for Foo object allocated
  2. Member Initialization - member is initialized (not assigned) with value a
    • This happens via the initializer list : member(a)
    • The const int member is created and given its value in one step
    • Since it's initialization (not assignment), it works with const!
  3. Constructor Body - The code inside { } executes
  4. Object Ready - Object is fully constructed and ready to use

What Happens WITHOUT Initializer List:

Foo(int a) {
    this->member = a;  // ❌ Trying to assign
}

Step-by-Step Flow:

  1. Memory Allocation - Space for Foo object allocated
  2. Member Default Construction - member is created but uninitialized (or default-initialized)
    • For const members, this is where they need their value!
    • But we didn't provide one via initializer list
  3. Constructor Body - Try to execute this->member = a;
    • ERROR! This is assignment, not initialization
    • Can't assign to a const variable!

Key Differences: Initialization vs Assignment

InitializationAssignment
Happens when variable is createdHappens after variable exists
Uses initializer list : member(value)Uses = operator in constructor body
Works with const members❌ Does NOT work with const members
Works with reference members❌ Does NOT work with reference members
More efficient (direct construction)Less efficient (construct then modify)

When You MUST Use Initializer Lists

You must use initializer lists for:

  1. Const member variables

    class Foo {
        const int x;
    public:
        Foo(int val) : x(val) { }  // Required!
    };
    
  2. Reference member variables

    class Foo {
        int& ref;
    public:
        Foo(int& r) : ref(r) { }  // Required!
    };
    
  3. Member objects without default constructors

    class Bar {
    public:
        Bar(int x) { }  // No default constructor
    };
    
    class Foo {
        Bar b;
    public:
        Foo() : b(10) { }  // Required! Bar needs a value
    };
    
  4. Base class initialization (inheritance)

    class Base {
    public:
        Base(int x) { }
    };
    
    class Derived : public Base {
    public:
        Derived(int x) : Base(x) { }  // Required!
    };
    

Best Practices

DO: Use initializer lists for all member variables

class Person {
    std::string name;
    int age;
public:
    Person(std::string n, int a) : name(n), age(a) { }
};

DO: Initialize members in the same order they are declared in the class

class Foo {
    int x;    // Declared first
    int y;    // Declared second
public:
    Foo(int a, int b) : x(a), y(b) { }  // Initialize in same order
};

DON'T: Mix initialization and assignment unnecessarily

// Bad - Inefficient
Foo(int a) {
    member = a;  // Default construct, then assign
}

// Good - Efficient
Foo(int a) : member(a) { }  // Direct initialization

↑ Back to Table of Contents


5. The this Pointer and Const Member Functions

Understanding the this Pointer

The this pointer is a hidden pointer that exists in every non-static member function. It points to the object that called the function.

How Member Functions Actually Work

When you write:

obj.print_obj();

The compiler secretly transforms this into something like:

print_obj(&obj);  // Pass the address of obj as a hidden argument

Inside the function, you access members through this hidden pointer called this.

The this Pointer Explained

  • this is a pointer to the object that called the member function
  • It's automatically passed to every non-static member function
  • Type: ClassName* const (constant pointer to the class type)
  • You can use it explicitly (this->member) or implicitly (member)

Why is this a Constant Pointer?

The type Foo* const means:

  • Foo* - Pointer to a Foo object
  • const (after the *) - The pointer itself is constant

This means:

  • ✓ You CAN modify the object that this points to (change member variables)
  • ✗ You CANNOT reassign this to point to a different object
void someFunction() {
    // this has type: Foo* const
    
    this->member = 10;     // ✓ OK: Can modify the object
    member = 20;           // ✓ OK: Same thing (implicit this)
    
    Foo other;
    this = &other;         // ❌ ERROR: Cannot reassign 'this'!
                          // 'this' is a constant pointer
}

Why this design? The this pointer must always point to the same object throughout the entire function execution. It would be dangerous and nonsensical to allow this to be reassigned to point to a different object mid-function!

The Problem with Const Objects

Consider this example:

#include <iostream>

class Foo {
    private:
        const int member;
    public:
        explicit Foo() : member(0) { 
            std::cout << "Foo() invoked\n"; 
        }
        
        explicit Foo(int a) : member(a) {
            std::cout << "Foo(int a) invoked\n";
        }
        
        ~Foo() {
            std::cout << "~Foo() invoked\n";
        }

        // Non-const member function
        void print_obj() {
            std::cout << "Object Add: " << this << ": member : " << this->member << std::endl;
        }
};

int main() {
    Foo obj1;
    obj1.print_obj();  // ✓ Works fine
    
    Foo obj2(2);
    obj2.print_obj();  // ✓ Works fine
    
    const Foo obj3(20);  // const object
    obj3.print_obj();    // ❌ COMPILATION ERROR!
    
    return 0;
}

Compilation Error

error: passing 'const Foo' as 'this' argument discards qualifiers

Why Does This Fail?

Let's understand what's happening behind the scenes:

  1. When you call obj3.print_obj() on a const object:

    • The compiler tries to pass &obj3 to print_obj()
    • Type of &obj3 is const Foo* (pointer to const Foo)
  2. What print_obj() expects:

    • Type: Foo* const (constant pointer to non-const Foo)
    • The function signature is really: void print_obj(Foo* const this)
    • This means this cannot be reassigned, but the object can be modified
  3. Type Mismatch:

    • You're trying to pass: const Foo*
    • Function expects: Foo* const
    • This is not allowed because it would discard the const qualifier!

Visualizing the Type Mismatch

void print_obj() {
    // Behind the scenes, this function signature is:
    // void print_obj(Foo* const this)
    //                ^^^^ ^^^^^
    //                |    |
    //                |    'this' pointer itself is constant (can't be reassigned)
    //                The object pointed to is non-const (can be modified)
}

const Foo obj3(20);
obj3.print_obj();
// Trying to pass: const Foo*
// Function expects: Foo* const
// ❌ ERROR: Cannot convert const Foo* to Foo* const
// The issue is the first 'const' - it protects the object from modification

Why is this dangerous? If allowed, you could modify a const object through the non-const this pointer, violating const-correctness!

The Solution: Const Member Functions

Mark the member function as const to tell the compiler: "This function will not modify the object."

Corrected Code

#include <iostream>

class Foo {
    private:
        const int member;
    public:
        explicit Foo() : member(0) { 
            std::cout << "Foo() invoked\n"; 
        }
        
        explicit Foo(int a) : member(a) {
            std::cout << "Foo(int a) invoked\n";
        }
        
        ~Foo() {
            std::cout << "~Foo() invoked\n";
        }

        // Const member function - note the 'const' after parameter list
        void print_obj() const {
            std::cout << "Object Add: " << this << ": member : " << this->member << std::endl;
        }
};

int main() {
    Foo obj1;
    obj1.print_obj();  // ✓ Works
    
    Foo obj2(2);
    obj2.print_obj();  // ✓ Works
    
    const Foo obj3(20);  // const object
    obj3.print_obj();    // ✓ Now works!
    
    return 0;
}

Output

Foo() invoked
Object Add: 0x16fdff04c: member : 0
Foo(int a) invoked
Object Add: 0x16fdff048: member : 2
Foo(int a) invoked
Object Add: 0x16fdff044: member : 20
~Foo() invoked
~Foo() invoked
~Foo() invoked

How const Fixes the Issue

Behind the Scenes: Function Signature

When you add const to a member function:

void print_obj() const {
    // Behind the scenes:
    // void print_obj(const Foo* const this)
    //                ^^^^^ ^^^   ^^^^^
    //                |     |     |
    //                |     |     'this' pointer is constant (can't be reassigned)
    //                |     pointer
    //                Object is const (cannot be modified)
}

The const keyword changes the type of the this pointer from Foo* const to const Foo* const.

Now:

  • The object pointed to by this is const (first const)
  • The pointer this itself is const (second const)

GDB Evidence

Using GDB with demangling turned off reveals the true function signature:

(gdb) set print demangle off
(gdb) info functions Foo::print_obj
All functions matching regular expression "Foo::print_obj":

File const.cpp:
22: void _ZNK3Foo9print_objEv(const Foo * const);
                ^^                ^^^^^
                ||                |||||
                ||                const Foo* const
                ||
                'K' indicates const member function

Breakdown of the mangled name _ZNK3Foo9print_objEv:

  • _Z = Start of mangled name
  • N = Nested name
  • K = const member function (this is the key!)
  • 3Foo = Class name "Foo" (3 characters)
  • 9print_obj = Function name "print_obj" (9 characters)
  • Ev = Return type void, no parameters (except hidden this)

The signature shows: void _ZNK3Foo9print_objEv(const Foo * const);

This means the function receives: const Foo* const

  • First const: The object pointed to cannot be modified
  • *: Pointer
  • Second const: The pointer itself cannot be reassigned

This matches what we expect for a const member function!

Type Matching with Const Member Functions

Without const keyword:

void print_obj() {
    // Real signature: void print_obj(Foo* const this)
    //                                 ^^^^ ^^^^^
    //                                 Can modify object, pointer is constant
}

const Foo obj3(20);
obj3.print_obj();
// Passing: const Foo* const
// Expects: Foo* const
// ❌ Type mismatch! The object being passed is const, but function could modify it

With const keyword:

void print_obj() const {
    // Real signature: void print_obj(const Foo* const this)
    //                                 ^^^^^ ^^^   ^^^^^
    //                                 Cannot modify object, pointer is constant
}

const Foo obj3(20);
obj3.print_obj();
// Passing: const Foo* const
// Expects: const Foo* const
// ✓ Types match perfectly!

What Const Member Functions Promise

When you declare a member function as const:

void print_obj() const {
    // Inside this function:
    // - 'this' has type: const Foo* const
    // - You CANNOT modify any member variables (object is const)
    // - You CANNOT reassign 'this' pointer (pointer is const)
    // - You CAN read member variables
    // - You CAN only call other const member functions
}

What You Can and Cannot Do

class Foo {
    int x;
    int y;
public:
    void readOnly() const {
        std::cout << x;     // ✓ OK: Reading is allowed
        std::cout << y;     // ✓ OK: Reading is allowed
        
        // x = 10;          // ❌ ERROR: Cannot modify members
        // y = 20;          // ❌ ERROR: Cannot modify members
    }
    
    void modify() {
        x = 10;             // ✓ OK: Non-const function can modify
    }
    
    void anotherConst() const {
        readOnly();         // ✓ OK: Can call const functions
        // modify();        // ❌ ERROR: Cannot call non-const functions
    }
};

Rules for Const Objects and Functions

ScenarioAllowed?Explanation
Non-const object calling non-const function✓ YesNormal case
Non-const object calling const function✓ YesSafe: const function won't modify
Const object calling const function✓ YesPerfect match: both are const
Const object calling non-const function❌ NoUnsafe: function might modify const object

Best Practices

DO: Mark member functions as const if they don't modify the object

class Person {
    std::string name;
    int age;
public:
    // Getters should be const - they only read data
    std::string getName() const { return name; }
    int getAge() const { return age; }
    
    // Setters should NOT be const - they modify data
    void setName(const std::string& n) { name = n; }
    void setAge(int a) { age = a; }
    
    // Display functions should be const - they only read
    void display() const {
        std::cout << name << " is " << age << " years old\n";
    }
};

DO: Use const-correctness throughout your code

void processUser(const Person& p) {
    p.display();    // ✓ OK: display() is const
    // p.setAge(30); // ❌ ERROR: setAge() is not const
}

DON'T: Forget to mark read-only functions as const

class Bad {
    int x;
public:
    int getValue() { return x; }  // ❌ Bad: Should be const!
};

void useIt(const Bad& b) {
    // int val = b.getValue();  // ❌ Won't compile!
}

↑ Back to Table of Contents


6. The mutable Keyword

The Problem: Wanting to Modify Some Members of Const Objects

Sometimes you have a const object where most members should be read-only, but a few specific members need to be modifiable. This is common in scenarios like:

  • Caching: Storing computed results to avoid recalculation
  • Debugging counters: Tracking how many times a function is called
  • Lazy initialization: Initializing data only when first accessed
  • Mutex locks: Managing thread synchronization in const member functions

Example Problem

#include <iostream>

class Foo {
    private:
        int member;
        int readonly_member;
    public:
        explicit Foo(int a, int b) : member(a), readonly_member(b) {
            std::cout << "Foo(int a, int b) invoked\n";
        }

        void print_obj() const {
            std::cout << "Object: " << this 
                      << ", member: " << member 
                      << ", readonly: " << readonly_member << std::endl;
        }
        
        void can_modify(int data) const {
            this->member = data;           // ❌ ERROR: Cannot modify in const function!
            // this->readonly_member = data; // ❌ ERROR: Cannot modify in const function!
        }
};

int main() {
    const Foo obj1(20, 30);
    obj1.print_obj();
    
    // I want to modify 'member' but keep the object const
    obj1.can_modify(100);  // ❌ Won't compile!
    
    return 0;
}

Compilation Error

error: assignment of member 'Foo::member' in read-only object

The Problem: Even though can_modify() is a const member function, it cannot modify ANY member variables because this has type const Foo* const.

The Solution: The mutable Keyword

The mutable keyword allows you to mark specific member variables as always modifiable, even in const member functions and const objects.

Syntax

class ClassName {
    mutable Type memberName;  // This member can be modified even in const contexts
};

Corrected Example

#include <iostream>

class Foo {
    private:
        mutable int member;        // mutable: can be modified even in const functions
        int readonly_member;       // regular: cannot be modified in const functions
    public:
        explicit Foo() : member(0), readonly_member(0) { 
            std::cout << "Foo() invoked\n"; 
        }
        
        explicit Foo(int a, int b) : member(a), readonly_member(b) {
            std::cout << "Foo(int a, int b) invoked\n";
        }
        
        ~Foo() {
            std::cout << "~Foo() invoked\n";
        }

        void print_obj() const {
            std::cout << "Object: " << this 
                      << ", member: " << member 
                      << ", readonly: " << readonly_member << std::endl;
        }
        
        void can_modify(int data) const {
            this->member = data;              // ✓ OK: member is mutable
            // this->readonly_member = data;  // ❌ ERROR: readonly_member is not mutable
        }
};

int main() {
    // Creating a constant object
    const Foo obj1(20, 30);
    std::cout << "Initial state:\n";
    obj1.print_obj();
    
    // Modifying the mutable member through a const function
    std::cout << "\nModifying mutable member to 100:\n";
    obj1.can_modify(100);
    obj1.print_obj();
    
    return 0;
}

Output

Foo(int a, int b) invoked
Initial state:
Object: 0x16fdff048, member: 20, readonly: 30

Modifying mutable member to 100:
Object: 0x16fdff048, member: 100, readonly: 30
~Foo() invoked

How mutable Works

When you mark a member as mutable:

class Foo {
    mutable int counter;  // Can be modified even in const functions
    int value;            // Cannot be modified in const functions
    
public:
    void someConstFunction() const {
        // this has type: const Foo* const
        
        counter++;     // ✓ OK: counter is mutable
        // value++;    // ❌ ERROR: value is not mutable
    }
};

Key Point: The mutable keyword essentially tells the compiler: "Don't apply const restrictions to this particular member, even when the object is const."

Real-World Use Cases

1. Caching Expensive Computations

class DataProcessor {
    std::vector<int> data;
    mutable bool cached;
    mutable double cachedResult;
    
public:
    DataProcessor(const std::vector<int>& d) 
        : data(d), cached(false), cachedResult(0.0) {}
    
    // This function doesn't logically modify the object,
    // but it caches the result for performance
    double getAverage() const {
        if (!cached) {
            double sum = 0;
            for (int val : data) sum += val;
            cachedResult = sum / data.size();  // ✓ OK: mutable
            cached = true;                      // ✓ OK: mutable
        }
        return cachedResult;
    }
};

2. Debug Counters

class Service {
    mutable int callCount;  // Track how many times methods are called
    std::string data;
    
public:
    Service(const std::string& d) : callCount(0), data(d) {}
    
    std::string getData() const {
        callCount++;  // ✓ OK: Track calls even in const function
        return data;
    }
    
    int getCallCount() const {
        return callCount;
    }
};

3. Lazy Initialization

class ExpensiveResource {
    mutable std::unique_ptr<Resource> resource;  // Initialized on first use
    
public:
    const Resource& getResource() const {
        if (!resource) {
            resource = std::make_unique<Resource>();  // ✓ OK: Lazy init
        }
        return *resource;
    }
};

4. Thread Synchronization

class ThreadSafeCounter {
    mutable std::mutex mtx;  // Mutex must be lockable in const functions
    int count;
    
public:
    int getCount() const {
        std::lock_guard<std::mutex> lock(mtx);  // ✓ OK: Can lock mutable mutex
        return count;
    }
    
    void increment() {
        std::lock_guard<std::mutex> lock(mtx);
        count++;
    }
};

Important Characteristics of mutable

What mutable Does:

  • ✓ Allows modification of the member in const member functions
  • ✓ Allows modification of the member in const objects
  • ✓ Exempts the member from const-correctness rules

What mutable Does NOT Do:

  • ✗ Does not make the member constant
  • ✗ Does not affect the member in non-const contexts
  • ✗ Does not change thread-safety characteristics

Comparison: Regular vs Mutable Members

class Example {
    int regular;
    mutable int mutableMember;
    
public:
    // Non-const member function
    void modify() {
        regular = 1;        // ✓ OK
        mutableMember = 2;  // ✓ OK
    }
    
    // Const member function
    void constModify() const {
        // regular = 1;        // ❌ ERROR
        mutableMember = 2;     // ✓ OK
    }
};

int main() {
    // Non-const object
    Example obj1;
    obj1.regular = 10;        // ✓ OK
    obj1.mutableMember = 20;  // ✓ OK
    
    // Const object
    const Example obj2;
    // obj2.regular = 10;        // ❌ ERROR
    // obj2.mutableMember = 20;  // ❌ ERROR: Direct access still not allowed
    
    // But mutable members CAN be modified through const member functions
    obj2.constModify();  // ✓ OK: Modifies mutableMember internally
}

When to Use mutable

DO use mutable for:

  • Internal caching mechanisms
  • Debug/logging counters
  • Lazy initialization
  • Synchronization primitives (mutexes)
  • Implementation details that don't affect logical const-ness

DON'T use mutable for:

  • Core data that defines the object's state
  • When it breaks the logical const-ness of the object
  • As a workaround for poor design
  • When a better design would avoid the need for it

Best Practices

Good Use: Caching

class MathProcessor {
    std::vector<int> numbers;
    mutable bool sumCached;
    mutable int cachedSum;
    
public:
    int getSum() const {
        if (!sumCached) {
            cachedSum = 0;
            for (int n : numbers) cachedSum += n;
            sumCached = true;
        }
        return cachedSum;
    }
};

Why it's good: The cache is an implementation detail. Logically, getSum() doesn't modify the object—it just returns a value.

Bad Use: Breaking Logical Const-ness

class Counter {
    mutable int count;  // ❌ Bad: count is the object's main state!
    
public:
    void increment() const {  // ❌ Bad: This should NOT be const!
        count++;
    }
};

Why it's bad: The count is the object's primary state. If you're modifying it, the object IS changing, so the function shouldn't be const.

↑ Back to Table of Contents

📘 Understanding Copy Constructors in C++

Let’s explore what a copy constructor is, when it’s invoked, and understand deep vs shallow copies and temporary objects through examples.


🧠 What is a Copy Constructor?

A copy constructor in C++ is a special constructor used to create a new object as a copy of an existing object.

📜 Syntax

ClassName(const ClassName& other);

⚙️ Purpose

  • Defines how an object should be copied.
  • Required when your class manages resources (like memory, files, sockets).
  • Prevents issues like double deletion and dangling pointers.

🧩 When is it Invoked?

The compiler automatically calls the copy constructor in these cases:

  1. Object initialization using another object

    Foo obj2 = obj1;   // or Foo obj2(obj1);
    
  2. Passing an object by value to a function

    void func(Foo obj); // Copy constructor called when passed by value
    
  3. Returning an object by value from a function

    Foo get_obj() {
        Foo temp(10);
        return temp; // Copy constructor may be invoked (before RVO)
    }
    
  4. Explicit copying using copy initialization

    Foo obj3 = Foo(obj1); // Explicit copy
    

If you do not define a copy constructor, the compiler provides a default shallow copy constructor, which may not be safe for classes managing dynamic memory.


🧩 Step 1: Basic Class Without Copy Constructor

#include <iostream>

class Foo {
private:
    int* ptr;

public:
    Foo(int value) {
        ptr = new int(value);
        std::cout << "Foo(int) invoked, *ptr = " << *ptr << "\n";
    }

    ~Foo() {
        std::cout << "~Foo() invoked, deleting ptr\n";
        delete ptr;
    }
};

int main() {
    Foo obj1(10);
    Foo obj2 = obj1;  // ❌ Problem here
    return 0;
}

🧨 Problem: Shallow Copy

The compiler automatically generates a default copy constructor that performs a member-wise (shallow) copy.
That means both obj1 and obj2 will have their ptr pointing to the same memory location. When both destructors run:

  • obj1 deletes ptr
  • obj2 also tries to delete the same memory
./a.out
Foo(int) invoked, *ptr = 10
~Foo() invoked, deleting ptr
~Foo() invoked, deleting ptr
a.out(53252,0x1f91d60c0) malloc: *** error for object 0x6000013a4020: pointer being freed was not allocated
a.out(53252,0x1f91d60c0) malloc: *** set a breakpoint in malloc_error_break to debug
[1]    53252 abort      ./a.out

💥 Result: Double free or corruption runtime error.


🧪 Step 2: What Valgrind(linux)/leaks(mac) Shows

If you run this under Valgrind, you’ll see:

==1234== Invalid free() / delete / delete[]
==1234==    at 0x4C2B5D5: operator delete(void*) (vg_replace_malloc.c:642)
==1234==    by 0x1091C2: Foo::~Foo() (example.cpp:12)
==1234==  Address 0x5a52040 is 0 bytes inside a block of size 4 free'd
==1234==    by 0x1091C2: Foo::~Foo() (example.cpp:12)
leaks --atExit -- ./a.out
a.out(56120) MallocStackLogging: could not tag MSL-related memory as no_footprint, so those pages will be included in process footprint - (null)
a.out(56120) MallocStackLogging: recording malloc (and VM allocation) stacks using lite mode
Foo(int) invoked, *ptr = 10
~Foo() invoked, deleting ptr
~Foo() invoked, deleting ptr
a.out(56120,0x1f91d60c0) malloc: *** error for object 0x133804080: pointer being freed was not allocated
a.out(56120,0x1f91d60c0) malloc: *** set a breakpoint in malloc_error_break to debug

This happens because two destructors delete the same pointer.


✅ Step 3: Add a Custom Copy Constructor (Deep Copy)

We fix this by allocating new memory for each object, and copying the value instead of the pointer.

#include <iostream>

class Foo {
private:
    int* ptr;

public:
    Foo(int value) {
        ptr = new int(value);
        std::cout << "Foo(int) invoked, *ptr = " << *ptr << "\n";
    }

    // 🟢 Copy Constructor (Deep Copy)
    Foo(Foo& obj) {
        ptr = new int(*obj.ptr);
        std::cout << "Foo(Foo&) invoked (deep copy), *ptr = " << *ptr << "\n";
    }

    ~Foo() {
        std::cout << "~Foo() invoked, deleting ptr\n";
        delete ptr;
    }
};

int main() {
    Foo obj1(10);
    Foo obj2 = obj1; // Deep copy now, no double delete
    return 0;
}

Now each object has its own ptr, and deletion is safe.


🔍 Step 4: Problem with Temporaries (rvalues or prvalues in c++11)

Let’s add a function that returns a temporary object:

Foo get_obj() {
    return Foo(20); // creates a temporary (prvalue)
}

int main() {
    Foo obj5 = get_obj(); // ❌ Error with Foo(Foo&)
    return 0;
}

❌ Error:

error: no matching constructor for initialization of 'Foo'
note: candidate constructor not viable: expects an lvalue for 1st argument

Why?

  • return Foo(20) creates a temporary object (a prvalue).
  • The parameter type Foo& cannot bind to a temporary object.
  • In C++, non-const lvalue references cannot bind to temporaries.

🧱 Step 5: Fix by Adding const to Copy Constructor

#include <iostream>

class Foo {
private:
    int* ptr;

public:
    Foo(int value) {
        ptr = new int(value);
        std::cout << "Foo(int) invoked, *ptr = " << *ptr << "\n";
    }

    // ✅ Const Copy Constructor
    Foo(const Foo& obj) {
        ptr = new int(*obj.ptr);
        std::cout << "Foo(const Foo&) invoked (deep copy), *ptr = " << *ptr << "\n";
    }

    ~Foo() {
        std::cout << "~Foo() invoked, deleting ptr\n";
        delete ptr;
    }
};

Foo get_obj() {
    return Foo(30);
}

int main() {
    Foo obj1(10);
    Foo obj2 = obj1;      // ✅ lvalue copy
    Foo obj3 = get_obj(); // ✅ prvalue copy
    return 0;
}

Now it works for both:

  • lvalues (obj1)
  • temporaries (prvalues) returned from functions

🧠 Step 6: Understanding Temporary Objects

💡 What is a Temporary (prvalue)?

  • Created by expressions like Foo(20) or return Foo().
  • Exists only until the end of the full expression.
  • Cannot be modified (non-const binding forbidden).

That’s why the copy constructor should accept:

Foo(const Foo& obj);

so that temporaries can be used to create new objects safely.


🕵️‍♂️ Step 7: Unoptimized Invocations

Before compiler optimizations (like Return Value Optimization, RVO),
the following may happen when you call get_obj():

  1. Foo(30) temporary created (constructor invoked)
  2. Temporary copied into obj3 (copy constructor invoked)
  3. Temporary destroyed (destructor invoked)
  4. obj3 destroyed (destructor invoked)

Output (unoptimized):

Foo(int) invoked, *ptr = 30
Foo(const Foo&) invoked (deep copy), *ptr = 30
~Foo() invoked, deleting ptr
~Foo() invoked, deleting ptr

In optimized builds, modern compilers often elide these copies (RVO),
so you might see fewer constructor calls.


🧾 Summary

ConceptDescription
Copy ConstructorSpecial constructor used to create an object as a copy of another object
Shallow CopyCopies pointer value → both objects share same memory → leads to double free
Deep CopyAllocates new memory and copies data → each object owns its own copy
Why const?Allows binding to temporaries (prvalues)
Without constFails when copying from a temporary
Temporary (prvalue)A short-lived unnamed object like Foo(10) or return Foo()

Next step 👉 Move Constructor
(to optimize performance and avoid unnecessary deep copies for temporaries).

↑ Back to Table of Contents

Summary

Constructors initialize objects after memory allocation, while destructors clean up resources before memory deallocation. Using the explicit keyword on constructors is a best practice that prevents implicit type conversions, making your code safer, clearer, and more maintainable.

Member initializer lists allow you to initialize member variables at the moment of their construction, which is essential for const and reference members, and more efficient for all member variables.

The this pointer is a hidden pointer passed to every member function that points to the calling object. When working with const objects, member functions must be marked as const to accept a const Foo* const instead of Foo* const, ensuring const-correctness and type safety.

The mutable keyword allows specific member variables to be modified even in const member functions and const objects. Use it for implementation details like caching, debug counters, and lazy initialization—but not for core object state.

Bottom Line: Use mutable judiciously for implementation details that don't affect the logical const-ness of your objects. It's a powerful tool for optimization and internal bookkeeping, but shouldn't be used to bypass const-correctness for core object state!

↑ Back to Table of Contents