C++ Constructors and Destructors - Complete Guide
Table of Contents
- Constructors
- Destructors
- The
explicitKeyword - Constructor Initializer Lists
- The
thisPointer and Const Member Functions - The
mutableKeyword - 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:
Foo obj1;- Calls the default constructor (no parameters)Foo obj2(2);- Calls the parameterized constructor with explicit syntaxFoo obj3 = 10;- Calls the parameterized constructor through implicit conversion frominttoFoo
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.
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.
3. The explicit Keyword
Why Implicit Conversions Are Problematic
Implicit conversions can lead to several issues:
- Unintended Behavior - The compiler silently converts types, which may not be what you intended
- Harder to Debug - When something goes wrong, it's difficult to trace back to an implicit conversion
- Reduces Code Clarity - Other developers reading your code may not realize a conversion is happening
- Potential Performance Issues - Unnecessary temporary objects may be created
- 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 explicit | With explicit |
|---|---|
Foo obj = 10; ✓ compiles | Foo obj = 10; ✗ error |
Foo obj(10); ✓ compiles | Foo obj(10); ✓ compiles |
| Implicit conversions allowed | Only explicit conversions allowed |
| Can hide bugs | Catches bugs at compile time |
| Less clear intent | Crystal clear intent |
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:
- Memory Allocation - Space for
Fooobject allocated - Member Initialization -
memberis initialized (not assigned) with valuea- This happens via the initializer list
: member(a) - The
const int memberis created and given its value in one step - Since it's initialization (not assignment), it works with
const!
- This happens via the initializer list
- Constructor Body - The code inside
{ }executes - 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:
- Memory Allocation - Space for
Fooobject allocated - Member Default Construction -
memberis created but uninitialized (or default-initialized)- For
constmembers, this is where they need their value! - But we didn't provide one via initializer list
- For
- Constructor Body - Try to execute
this->member = a;- ❌ ERROR! This is assignment, not initialization
- Can't assign to a
constvariable!
Key Differences: Initialization vs Assignment
| Initialization | Assignment |
|---|---|
| Happens when variable is created | Happens 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:
-
Const member variables
class Foo { const int x; public: Foo(int val) : x(val) { } // Required! }; -
Reference member variables
class Foo { int& ref; public: Foo(int& r) : ref(r) { } // Required! }; -
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 }; -
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
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
thisis 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 aFooobjectconst(after the*) - The pointer itself is constant
This means:
- ✓ You CAN modify the object that
thispoints to (change member variables) - ✗ You CANNOT reassign
thisto 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:
-
When you call
obj3.print_obj()on aconstobject:- The compiler tries to pass
&obj3toprint_obj() - Type of
&obj3isconst Foo*(pointer to const Foo)
- The compiler tries to pass
-
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
thiscannot be reassigned, but the object can be modified
- Type:
-
Type Mismatch:
- You're trying to pass:
const Foo* - Function expects:
Foo* const - This is not allowed because it would discard the
constqualifier!
- You're trying to pass:
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
thisis const (firstconst) - The pointer
thisitself is const (secondconst)
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 nameN= Nested nameK= 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 hiddenthis)
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
| Scenario | Allowed? | Explanation |
|---|---|---|
| Non-const object calling non-const function | ✓ Yes | Normal case |
| Non-const object calling const function | ✓ Yes | Safe: const function won't modify |
| Const object calling const function | ✓ Yes | Perfect match: both are const |
| Const object calling non-const function | ❌ No | Unsafe: 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!
}
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
constmember functions - ✓ Allows modification of the member in
constobjects - ✓ 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.
📘 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:
-
Object initialization using another object
Foo obj2 = obj1; // or Foo obj2(obj1); -
Passing an object by value to a function
void func(Foo obj); // Copy constructor called when passed by value -
Returning an object by value from a function
Foo get_obj() { Foo temp(10); return temp; // Copy constructor may be invoked (before RVO) } -
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:
obj1deletesptrobj2also 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)orreturn 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():
Foo(30)temporary created (constructor invoked)- Temporary copied into
obj3(copy constructor invoked) - Temporary destroyed (destructor invoked)
obj3destroyed (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
| Concept | Description |
|---|---|
| Copy Constructor | Special constructor used to create an object as a copy of another object |
| Shallow Copy | Copies pointer value → both objects share same memory → leads to double free |
| Deep Copy | Allocates new memory and copies data → each object owns its own copy |
Why const? | Allows binding to temporaries (prvalues) |
Without const | Fails 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!