← Back to list

What Happens In C++ Program, If We Do Not Want To Use The Rule of 3?

Basically if do not want to keep record of the dynamically allocated memory and then explicitly delete it in destructor?

Rohit Jagtap · 2025-05-22 19:22 · 10 claps · 8.2 min read
#cpp-programming #smart-pointer #move-semantics #operator-overloading #friend-function
Open on Medium ↗
Wiki topics: LNG · Linguistics & Language 💻 · Programming

What Happens In C++ Program, If We Do Not Want To Use The Rule of 3? Or If We Do not Want To Use The Destructor Explicitly?

Basically if do not want to keep record of the dynamically allocated memory and then explicitly delete it in destructor?

Details if we do not want to use the deep copy constructor, assignment operator & destructor to handle the undefined behavior, double deletion, memory leak, dangling pointer then how we can solve the problems? To find this out, then read this article till the end.

To check Details about the Copy Constructors, Copy Assignment Operator & Destructor (Rule of 3), please click here.

What is output for below C++ program?

#include <iostream>

class A{
  public:
    int* a;

    //Constructor
    A(int val){
      a = new int(val);
    }

    //Deep Copy Constructor
    A(const A& other){
      a = new int(*(other.a));
    }

    //Deep Copy Assignment Operator
    A& operator=(const A& other){
      if(this != &other){
        delete this->a;
        this->a = new int(*(other.a));
      }
      return *this;
    }

    void modify(int val){
      *a = val;      
    }

    //Destructor
    ~A() {
      delete a;
    }
};

int main(){
  A obj1(5);
  A obj2 = obj1;

  obj1.modify(25);

  A obj3(6);
  obj3 = obj1;

  std::cout<< *(obj1.a) << " " << *(obj2.a) <<" "<< *(obj3.a) << std::endl;
  std::cout<< obj1.a << " " << obj2.a <<" "<< obj3.a << std::endl;

  return 0;
}

Output:

25 5 25
0x5b9469c552b0 0x5b9469c552d0 0x5b9469c552f0

What will be the peer review comments on the above program? Lets take a look:

  1. Good use of the Rule of Three — copy constructor, copy assignment operator, and destructor are all implemented.
  2. Deep copy logic is correct and avoids shallow copy issues.
  3. Destructor properly deletes dynamically allocated memory.
  4. Prefer Smart Pointers over Raw Pointers
  5. Add a Move Constructor and Move Assignment Operator
  6. Use Initialization Lists in Constructors
  7. Consider Renaming Member a
  8. Consider Renaming Class NameA

What is Smart Pointers?

It is a wrapper around a raw pointer (T*) that behaves like a pointer but with automatic cleanup and ownership semantics.

A smart pointer is a C++ object that manages the lifetime of a dynamically allocated resource, typically a pointer. It automatically releases the resource when it’s no longer needed, avoiding common memory issues like:

  • Memory leaks
  • Dangling pointers
  • Double deletions

Types of Smart Pointers (C++11 and later)

Unique Pointer:

  1. std::unique_ptr<T>
  2. Exclusive ownership of a pointer.
  3. Cannot be copied, only moved.
  4. Automatically deletes the object when it goes out of scope.
std::unique_ptr<int> a = std::make_unique<int>(42);

Shared Pointer:

  1. std::shared_ptr<T>
  2. Shared ownership — multiple smart pointers can point to the same object.
  3. Keeps a reference count internally.
  4. Object is deleted when the last shared_ptr goes out of scope.
std::shared_ptr<int> a1 = std::make_shared<int>(42);
std::shared_ptr<int> a2 = a1;  // Shared ownership

Weak Pointer:

  1. std::weak_ptr<T>
  2. Used to break circular references with shared_ptr.
  3. Doesn’t own the object — just observes it.
  4. Must be converted to shared_ptr to access the resource.
std::weak_ptr<int> wp = a1;  // a1 is a shared_ptr

For the common memory issues, we can use the smart pointers, but to resolve the main issue: undefined behavior when one object change affects another for this out of different smart pointer types we can use the unique pointer.

Why only Unique Pointer?

  1. Shared Pointer (std::shared_ptr): Allows shared ownership — changes in one object reflect in all owners.
  2. Weak Pointer (std::weak_ptr): Not even a full smart pointer — just an observer.
#include <iostream>
#include <memory>

class A{
  public:
    std::unique_ptr<int> a; //int* a;

    //Constructor with initilizer list
    A(int val): a(std::make_unique<int>(val)){}

    //Constructor
    //A(int val){
    //  a = new int(val);
    //}

    //Deep Copy Constructor
    //A(const A& other){
    //  a = new int(*(other.a));
    //}

    //Deep Copy Assignment Operator
    //A& operator=(const A& other){
    //  if(this != &other){
    //    delete this->a;
    //    this->a = new int(*(other.a));
    //  }
    //  return *this;
    //}

    void modify(int val){
      *a = val;      
    }

    //Destructor
    //~A() {
    //  delete a;
    //}
};

int main(){
  A obj1(5);
  A obj2 = obj1;

  obj1.modify(25);

  A obj3(6);
  obj3 = obj1;

  std::cout<< *(obj1.a) << " " << *(obj2.a) <<" "<< *(obj3.a) << std::endl;
  std::cout<< obj1.a << " " << obj2.a <<" "<< obj3.a << std::endl;

  return 0;
}

What is output of above program? compiler errors? why?

Case 1: Compilation error

Compile Error

main.cpp: In function ‘int main()’:
main.cpp:42:12: error: use of deleted function ‘A::A(const A&)’
   42 |   A obj2 = obj1;
      |            ^~~~

The Unique Pointer(std::unique_ptr) deletes its copy constructor and assignment operator.

So your class becomes non-copyable by default.

How to solve this problem now?

This is actually a safety feature.

If C++ allowed copying of std::unique_ptr, it would lead to shallow copies, which are:

  • Unsafe
  • Error-prone
  • Cause double deletes, dangling pointers, etc.

A) By forcing you to write deep copy logic, std::unique_ptr helps you avoid all these classic memory issues.

#include <iostream>
#include <memory>

class A{
  public:
    std::unique_ptr<int> a; //int* a;

    //Constructor with initilizer list
    A(int val): a(std::make_unique<int>(val)){}

    //Constructor
    //A(int val){
    //  a = new int(val);
    //}

    A(const A& other): a(std::make_unique<int>(*(other.a))){}
    //Deep Copy Constructor
    //A(const A& other){
    //  a = new int(*(other.a));
    //}

    A& operator=(const A& other){
      if(this != &other){
        a = std::make_unique<int>(*(other.a));
      }
      return *this;
    }
    //Deep Copy Assignment Operator
    //A& operator=(const A& other){
    //  if(this != &other){
    //    delete this->a;
    //    this->a = new int(*(other.a));
    //  }
    //  return *this;
    //}

    void modify(int val){
      *a = val;      
    }

    //Destructor
    //~A() {
    //  delete a;
    //}
};

int main(){
  A obj1(5);
  A obj2 = obj1;

  obj1.modify(25);

  A obj3(6);
  obj3 = obj1;

  std::cout<< *(obj1.a) << " " << *(obj2.a) <<" "<< *(obj3.a) << std::endl;
  std::cout<< obj1.a << " " << obj2.a <<" "<< obj3.a << std::endl;

  return 0;
}

What is output of above program? compiler errors? why?

main.cpp: In function ‘int main()’:
main.cpp:57:12: error: no match for ‘operator<<’ (operand types are ‘std::ostream’ {aka ‘std::basic_ostream<char>’} and ‘std::unique_ptr<int>’)
   57 |   std::cout<< obj1.a << " " << obj2.a <<" "<< obj3.a << std::endl;
      |   ~~~~~~~~~^~ ~~~~~~
      |        |           |
      |        |           std::unique_ptr<int>
      |        std::ostream {aka std::basic_ostream<char>}
main.cpp:57:12: note: candidate: ‘operator<<(int, int)’ (built-in)
   57 |   std::cout<< obj1.a << " " << obj2.a <<" "<< obj3.a << std::endl;
      |   ~~~~~~~~~^~~~~~~~~

The obj1.a is a std::unique_ptr<int>, and **std::unique_ptr does not overload the << operator** for printing directly.

  • The << operator knows how to print basic types (like int, std::string, etc.)
  • It does not know how to print a unique_ptr directly

That’s why you get a “no match for operator<<” compile-time error.

How to solve this problem now?

Option-1 : Print the underlying pointer:

std::cout<< obj1.a.get() << " " << obj2.a.get() <<" "<< obj3.a.get() << std::endl;

Option-2 : Print the value the unique_ptr points to:

if (obj1.a) {
    std::cout << *(obj1.a) << std::endl;
} else {
    std::cout << "obj1.a is null" << std::endl;
}

Option-3: Define your own operator<< for the class:

Instead of doing like option-2, you can use this operator overload method.

 //custom operator<<
 std::ostream& operator<<(std::ostream& os, const A& other){
  if(other.a){
   os << *(other.a)
  }
  else{
   os << "null";
  }
  return os;
 }

If we define like above as member function in class then it fails, if you print the object as:

int main(){
  A obj1(5);
  A obj2 = obj1;

  obj1.modify(25);

  A obj3(6);
  obj3 = obj1;

  std::cout<< obj1 << std::endl;

  return 0;
}

The line you write to print object:

std::cout<< obj1 << std::endl;

The compiler interprets as:

operator<<(std::cout, obj1);

so the operator function must look like:

std::ostream& operator<<(std::ostream& os, const A& obj);

This is a non-member function that takes:

  • The left-hand side (std::cout) as the first argument
  • Your object (obj1) as the second

And The member function takes:

  • A member function would only take one argument (because this is the first implicit one). and due to this above member function operator<< gives error
main.cpp:38:23: error: ‘std::ostream& A::operator<<(std::ostream&, const A&)’ must have exactly one argument
   38 |         std::ostream& operator<<(std::ostream& os, const A& other){
      |                       ^~~~~~~~

To solve this member function problem of operator<<, we need to define this as non member function (as a friend function)

#include <iostream>
#include <memory>

class A{
  public:
    std::unique_ptr<int> a; //int* a;

    //Constructor with initilizer list
    A(int val): a(std::make_unique<int>(val)){}

    //Constructor
    //A(int val){
    //  a = new int(val);
    //}

    A(const A& other): a(std::make_unique<int>(*(other.a))){}
    //Deep Copy Constructor
    //A(const A& other){
    //  a = new int(*(other.a));
    //}

    A& operator=(const A& other){
      if(this != &other){
        a = std::make_unique<int>(*(other.a));
      }
      return *this;
    }
    //Deep Copy Assignment Operator
    //A& operator=(const A& other){
    //  if(this != &other){
    //    delete this->a;
    //    this->a = new int(*(other.a));
    //  }
    //  return *this;
    //}

    //custom operator<<
    friend std::ostream& operator<<(std::ostream& os, const A& other);

    void modify(int val){
      *a = val;      
    }

    //Destructor
    //~A() {
    //  delete a;
    //}
};

std::ostream& operator<<(std::ostream& os, const A& other){
  if(other.a){
    os << *(other.a);
  }
  else{
    os << "null";
  }
  return os;
}

int main(){
  A obj1(5);
  A obj2 = obj1;

  obj1.modify(25);

  A obj3(6);
  obj3 = obj1;

  //std::cout<< *(obj1.a) << " " << *(obj2.a) <<" "<< *(obj3.a) << std::endl;
  //std::cout<< obj1.a << " " << obj2.a <<" "<< obj3.a << std::endl;

  std::cout<< obj1 << " " << obj2 <<" "<< obj3 << std::endl;
  std::cout<< obj1.a.get() << " " << obj2.a.get() <<" "<< obj3.a.get() << std::endl;

  return 0;
}

Output:

25 5 25
0x594c88baf2b0 0x594c88baf2d0 0x594c88baf310

B) Move semantics:

If your object is:

  • Not meant to be copied (e.g., it represents a unique resource like a file handle, socket, etc.)
  • Only ever moved between scopes or containers

Then you don’t need deep copy logic at all.

Move Constructor:

A(A&&) = default;

Move Assignment Operator:

A& operator=(A&&) = default;

If we are defining the above then we need to delete the copy constructor and assignment operator

 // Delete copy constructor and copy assignment to avoid accidental copies
 A(const A&) = delete;
 A& operator=(const A&) = delete; 

Is below program compile if we provide the move constructor and assignment operator: (Addressed PR comments: Using smart pointer, move semantics, correct variable & class names)

#include <iostream>
#include <memory>

class SmartDemo{
  public:
    std::unique_ptr<int> dataPtr; //int* dataPtr;

    //Constructor with initilizer list
    SmartDemo(int val): dataPtr(std::make_unique<int>(val)){}

    //Constructor
    //SmartDemo(int val){
    //  dataPtr = new int(val);
    //}

    //Default Move constructor & Move Assignment
    SmartDemo(SmartDemo&&) = default;
    SmartDemo& operator=(SmartDemo&&) = default;

    // Delete copy constructor and copy assignment to avoid accidental copies
    SmartDemo(const SmartDemo&) = delete;
    SmartDemo& operator=(const SmartDemo&) = delete;     

    //Copy Constructor with initilizer list
    //SmartDemo(const SmartDemo& other): dataPtr(std::make_unique<int>(*(other.a))){}
    //Deep Copy Constructor
    //SmartDemo(const SmartDemo& other){
    //  dataPtr = new int(*(other.dataPtr));
    //}

    //Deep Copy Assignment Operator with smart pointer
    //SmartDemo& operator=(const SmartDemo& other){
    //  if(this != &other){
    //    dataPtr = std::make_unique<int>(*(other.dataPtr));
    //  }
    //  return *this;
    //}

    //Deep Copy Assignment Operator
    //SmartDemo& operator=(const SmartDemo& other){
    //  if(this != &other){
    //    delete this->dataPtr;
    //    this->dataPtr = new int(*(other.dataPtr));
    //  }
    //  return *this;
    //}

    //custom operator<<
    friend std::ostream& operator<<(std::ostream& os, const SmartDemo& other);

    void modify(int val){
      *dataPtr = val;      
    }

    //Destructor
    //~SmartDemo() {
    //  delete dataPtr;
    //}
};

std::ostream& operator<<(std::ostream& os, const SmartDemo& other){
  if(other.dataPtr){
    os << *(other.dataPtr);
  }
  else{
    os << "null";
  }
  return os;
}

int main(){
  SmartDemo obj1(5);
  //SmartDemo obj2 = obj1;           //Error: copy constructor is deleted
  SmartDemo obj2 = std::move(obj1);  //Move constructor

  //obj1.modify(25);                 //Segmentation fault
  if(obj1.dataPtr)
    obj1.modify(25);                 //Works

  SmartDemo obj3(6);
  //obj3 = obj1;                     //Error: copy constructor is deleted
  obj3 = std::move(obj1);            //Move Assignment operator

  //std::cout<< *(obj1.dataPtr) << " " << *(obj2.dataPtr) <<" "<< *(obj3.dataPtr) << std::endl;
  //std::cout<< obj1.dataPtr << " " << obj2.dataPtr <<" "<< obj3.dataPtr << std::endl;

  std::cout<< obj1 << " " << obj2 <<" "<< obj3 << std::endl;
  std::cout<< obj1.dataPtr.get() << " " << obj2.dataPtr.get() <<" "<< obj3.dataPtr.get() << std::endl;

  return 0;
}

Output:

null 5 null
0 0x57d3677812b0 0

When to Use which Deep Copy/Move Semantics?

When to Use Move Semantics:

  • Your class owns exclusive resources (e.g. file handles, sockets, memory).
  • Copying would be expensive, dangerous, or illogical.
  • You only want one owner of a resource.
  • You’re using std::unique_ptr.

When You Should Avoid Move Only:

  • When your object must be copied (API requirements, container storage).

When to Use Deep Copy:

  • You need copies of objects with separate internal states.
  • Your object is stored in containers like std::vector<A>, which copy elements.
  • You’re passing objects by value in APIs.
  • You’re not using smart pointers, or using shared_ptr with care.

When You Should Avoid Deep Copy:

  • When the object manages a heavy resource (file, database connection, GPU memory).
  • When copying isn’t meaningful or needed.

When to Use both (Move & Deep Copy):


+--------------------------------------------------+----------------------------------------------------+------------------------------------------------------+
|                    Situation                     |                   Why deep copy?                   |                      Why move?                       |
+--------------------------------------------------+----------------------------------------------------+------------------------------------------------------+
| Class holds resources (e.g. unique_ptr, new int) | You need to copy the object with independent state | You want fast transfers of ownership without copying |
| Used in standard containers (std::vector<A>)     | Vectors copy elements during resize or insert      | Vectors also use move when possible (C++11+)         |
| The object must be passed by value in APIs       | Deep copy ensures the callee gets its own copy     | Move allows optimization when passing temporaries    |
| You want your class to behave like a value type  | Safe copying logic for business rules              | Avoid performance hit of copying large data          |
+--------------------------------------------------+----------------------------------------------------+------------------------------------------------------+

메타데이터
post_id
74dbd46ace22
slug
what-happens-if-in-c-program-we-do-not-want-to-use-the-rule-of-3-74dbd46ace22
url
https://medium.com/@rohit.jagtap10/what-happens-if-in-c-program-we-do-not-want-to-use-the-rule-of-3-74dbd46ace22
canonical_url
https://medium.com/@rohit.jagtap10/what-happens-if-in-c-program-we-do-not-want-to-use-the-rule-of-3-74dbd46ace22
author_url
https://medium.com/@rohit.jagtap10
status
ok
fetched_at
2026-07-21 11:03:45