Categories
Computer Science / Information Technology Language: C++

Smart Pointers

Overview

Raw pointers are a powerful tool in C++. They provide us with absolute flexibility with memory management. With this flexibility comes complexity. We must explicitly allocate and deallocate storage for heap dynamic variables as well as manage their lifetimes. And that’s where we often see problems. Infact, most defects in programs written in programming languages that provide raw pointers are pointer-related defects.

Some of the issues with raw pointers include:

  • Uninitialised (wild pointers): Pointers can point anywhere in memory. If such pointers are dereferenced, it can have catastrophic consequences.
  • Memory leaks: If a raw pointer is not properly deleted, it can lead to a memory leak. Memory leaks can cause programs to run out of memory and crash.
  • Dangling pointers: A dangling pointer is a pointer that points to an object that has been deleted. Dangling pointers can cause undefined behaviour, which can lead to crashes or other unexpected results.
  • Null pointers: A null pointer is a pointer that does not point to any object. Trying to dereference a null pointer can cause a crash.
  • Not exception safe: Our code might throw an exception and the code that releases our allocated memory may never execute leading to memory leak.

Smart pointers can help us prevent all of these types of errors. They can help us be more clear about who owns the pointer, and when a pointer should be deleted to free up allocated memory while being intuitive to use.

Smart pointers are a type of pointer that automatically manages the lifetime of the object it points to. This means that the smart pointer will automatically delete the object when it is no longer needed, preventing memory leaks. There are a few different types of smart pointers in C++, some of the most common are:

  • std::unique_ptr: A smart pointer that owns the object it points to. Only one std::unique_ptr can own an object at a time.
  • std::shared_ptr: A smart pointer that shares ownership of the object it points to. Multiple std::shared_ptrs can own the same object.
  • std::weak_ptr: A smart pointer that does not own the object it points to. A std::weak_ptr can only be used to check if the object it points to is still alive.

The best type of smart pointer to use depends on the specific situation. In general, std::unique_ptr should be used when an object should only be owned by one thing, std::shared_ptr should be used when an object should be shared by multiple things, and std::weak_ptr should be used when an object does not need to be owned. We shall see each of these in detail in upcoming sections.

Here are some of the disadvantages of using smart pointers:

  • Can be more complex than raw pointers: Smart pointers can be more complex to use than raw pointers, especially when multiple smart pointers are used to point to the same object.
  • Can be slower than raw pointers: Smart pointers can be slower than raw pointers, especially when they are used to share ownership of an object.
  • Cannot do pointer arithmetic.

Overall, smart pointers are a powerful tool that can help to improve the safety and reliability of C++ programs. However, it is important to be aware of the potential disadvantages of using smart pointers before using them in the code.

RAII: Resource Acquisition Is Initialisation

RAII stands for Resource Acquisition is Initialization. It is a programming idiom that can be used to automatically manage resources in C++. RAII works by associating a resource with a scope. When the scope exits, the resource is automatically released. This ensures that resources are always released correctly, even if there are errors in the program.

There are a few different ways to implement RAII in C++. One common way is to use smart pointers. Smart pointers are objects that automatically manage the lifetime of the resources they point to. When a smart pointer goes out of scope, it will automatically delete the resource it points to.

Another way to implement RAII is to use RAII classes. RAII classes are classes that encapsulate a resource, usually allocate the necessary resources in a constructor and automatically release it when the class goes out of scope. This usually happens in the class destructor. For example, the following class encapsulates a file handle:

class File {
public:
  File(const char* filename) : handle(fopen(filename, "r")) {}
  ~File() {
    if (handle) {
      fclose(handle);
    }
  }

private:
  FILE* handle;
};

This class can be used to open a file and automatically close it when the class goes out of scope. For example:

{
  File file("myfile.txt");
  // Do something with the file
}

When the File object goes out of scope, the fclose() function will be called to close the file. This ensures that the file is always closed correctly, even if there are errors in the program.

RAII is a powerful tool that can help to improve the safety and reliability of C++ programs. By using RAII, we can avoid memory leaks and other resource management problems.

Unique pointer

A unique pointer is an efficient and the fastest smart pointer (however, slower than the raw pointers). It owns the object it points to. Only one unique pointer can own an object at a time. When the unique pointer goes out of scope, the object it points to will be automatically deleted. The syntax to create a unique pointer is as follows:

std::unique_ptr<MyClass> my_ptr(new MyClass());

Here,

  • MyClass is the type of object that the unique pointer will point to.
  • new MyClass() creates a new MyClass object and returns a pointer to it.
  • The unique pointer my_ptr is then initialised with this pointer.

Once a unique pointer is created, it can be used to access the object it points to just like a raw pointer. For example:

// Access the object
my_ptr->some_method();

// Get the value of a member variable
int value = my_ptr->some_member_variable;

When the unique pointer goes out of scope, the object it points to will be automatically deleted. This means that you don’t have to worry about memory leaks.

Here is an example of how to use a unique pointer:

#include <iostream>
#include <memory>

class MyClass {
public:
  void some_method() {
    std::cout << "Hello, world!" << std::endl;
  }
};

int main() {
  std::unique_ptr<MyClass> my_ptr(new MyClass());
  my_ptr->some_method();
  return 0;
}

Output:

Hello, world!

The my_ptr unique pointer will automatically delete the MyClass object when it goes out of scope at the end of main(). This ensures that the MyClass object is properly cleaned up.

The std::make_unique()

std::make_unique is a C++14 library function template that creates a std::unique_ptr object and initialises it with a dynamically allocated object of the specified type. It is a convenient way to allocate and initialise an object using smart pointers.

The std::make_unique function simplifies the process of creating a std::unique_ptr by automatically allocating memory and constructing the object in a single step. It ensures exception safety by guaranteeing that the allocated memory will be properly deallocated if an exception occurs during object construction.

Here’s an example that demonstrates the usage of std::make_unique:

#include <memory>

class MyClass {
public:
    MyClass() {
        // Constructor logic
    }
};

int main() {
    // Using make_unique to create a unique_ptr object
    std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();

    // Accessing members of the dynamically allocated object
    ptr->someMethod();

    return 0;
}

In the above example, std::make_unique is used to create a std::unique_ptr object named ptr that points to a dynamically allocated MyClass object. The std::make_unique function automatically handles memory allocation and construction of the object. The ptr object can then be used to access members and methods of the allocated object.

Using std::make_unique instead of directly calling new and constructing the object separately provides several benefits, such as improved code readability, exception safety, and prevention of memory leaks. It is generally considered a good practice to use std::make_unique whenever possible to manage dynamically allocated objects with std::unique_ptr.

Shared pointer

A shared pointer is a smart pointer that shares ownership of the object it points to. Multiple shared pointers can own the same object. When the last shared pointer that owns an object goes out of scope, the object will be automatically deleted.

The syntax to create a shared pointer is as follows:

std::shared_ptr<MyClass> my_ptr(new MyClass());

Here, MyClass is the type of object that the shared pointer will point to. new MyClass() creates a new MyClass object and returns a pointer to it. The shared pointer my_ptr is then initialised with this pointer.

Once a shared pointer is created, it can be used to access the object it points to just like a raw pointer. For example:

// Access the object
my_ptr->some_method();

// Get the value of a member variable
int value = my_ptr->some_member_variable;

When the last shared pointer that owns an object goes out of scope, the object will be automatically deleted. This means that you don’t have to worry about memory leaks.

Here is an example of how to use a shared pointer:

#include <iostream>
#include <memory>

class MyClass {
public:
  void some_method() {
    std::cout << "Hello, world!" << std::endl;
  }
};

int main() {
  std::shared_ptr<MyClass> my_ptr(new MyClass());
  my_ptr->some_method();
  return 0;
}

Output:

Hello, world!

The my_ptr shared pointer will automatically delete the MyClass object when it goes out of scope at the end of main(). This ensures that the MyClass object is properly cleaned up. An added advantage of shared pointer is that it allows Multiple ownership: Multiple shared pointers can own the same object. This can be useful for objects that need to be shared between different parts of a program.

Weak pointer

A  weak pointer does not own the object it points to. A weak pointer can only be used to check if the object it points to is still alive. When the object that a weak pointer points to is deleted, the weak pointer will become invalid and will not be able to access the object.

The syntax to create a weak pointer is as follows:

std::weak_ptr<MyClass> my_ptr(new MyClass());

Here, MyClass is the type of object that the weak pointer will point to. new MyClass() creates a new MyClass object and returns a pointer to it. The weak pointer my_ptr is then initialised with this pointer.

Once a weak pointer has been created, it can be used to check if the object it points to is still alive using the expired() function. For example:

if (!my_ptr.expired()) {
  // The object is still alive.
} else {
  // The object has been deleted.
}

A weak pointer can be used to obtain a shared pointer to the object it points to if the object is still alive. For example:

std::shared_ptr<MyClass> my_shared_ptr = my_ptr.lock();

If the object is no longer alive, then my_shared_ptr will be a null pointer. Here is an example of how to use a weak pointer:

#include <iostream>
#include <memory>

class MyClass {
public:
  ~MyClass() {
    std::cout << "Object deleted." << std::endl;
  }
};

int main() {
  std::weak_ptr<MyClass> my_ptr(new MyClass());
  if (!my_ptr.expired()) {
    // The object is still alive.
  } else {
    // The object has been deleted.
  }

  std::shared_ptr<MyClass> my_shared_ptr = my_ptr.lock();
  if (my_shared_ptr) {
    // The object is still alive.
  } else {
    // The object has been deleted.
  }

  return 0;
}

Output:

Object deleted.

The my_ptr weak pointer will become invalid when the MyClass object is deleted. This will cause the if (!my_ptr.expired()) condition to evaluate to false. The my_shared_ptr shared pointer will also become invalid when the MyClass object is deleted. This will cause the if (my_shared_ptr) condition to evaluate to false.

Here are some of the advantages of using weak pointers:

  • Can be used to check if an object is still alive: Weak pointers can be used to check if an object is still alive. This can be useful for things like garbage collection and deferred cleanup.
  • Do not prevent objects from being deleted: Weak pointers do not prevent objects from being deleted. This means that they can be used to safely share ownership of an object with other objects.
  • Can be used to obtain a shared pointer to an object: Weak pointers can be used to obtain a shared pointer to an object if the object is still alive. This can be useful for things like accessing the object’s members or calling its methods.

Common operations

Here are some of the common operations that can be performed on smart pointers:

Move assignment

The move assignment operator can be used to move the ownership of an object from one smart pointer to another. For example:

std::unique_ptr<MyClass> my_new_ptr = std::move(my_old_ptr);

This will move the ownership of the object pointed to by my_old_ptr to my_new_ptr. my_old_ptr will no longer point to any object.

Reset

The reset() function can be used to reset a smart pointer to point to a new object. For example:

my_ptr.reset(new MyClass());

This will reset my_ptr to point to a new MyClass object. The old object that my_ptr was pointing to will be deleted.

Get

The get() function can be used to get the raw pointer that is encapsulated by a smart pointer. For example:

MyClass* raw_ptr = my_ptr.get();

This will return the raw pointer that my_ptr is pointing to. It is important to note that you should not delete the object that the raw pointer points to. This will be done automatically by the smart pointer when it goes out of scope.

Release

The release() function can be used to release ownership of an object from a smart pointer. For example:

MyClass* raw_ptr = my_ptr.release();

This will release ownership of the object that my_ptr is pointing to. The smart pointer will no longer point to any object. It is important to note that you are responsible for deleting the object that raw_ptr points to.

Custom deleter

A custom deleter is a function or a callable object that is used to release the memory allocated to an object managed by a smart pointer when the pointer goes out of scope. It is an optional parameter that can be passed to the constructor of the smart pointer. The custom deleter is executed instead of the default destructor of the smart pointer when the pointer is destroyed.

The syntax of custom deleter is as follows:

std::shared_ptr<T> ptr(new T, Deleter);

where T is the type of the object that the smart pointer is pointing to, and Deleter is a function object or a function pointer that is used to delete the object when it is no longer needed.

The syntax for defining a custom deleter function is:

void customDeleter(T* ptr) {
    // Custom deletion code here
}

Here, T is the type of the object that the custom deleter function will delete. The custom deletion code can be anything that is appropriate for the object being deleted, such as calling a member function, deallocating dynamic memory, or doing nothing at all.

To use the custom deleter function with a smart pointer, you pass it as the second argument to the std::shared_ptr constructor:

std::shared_ptr<T> ptr(new T, customDeleter);

This creates a std::shared_ptr object that owns the dynamically allocated object of type T, and uses the customDeleter function to delete it when the std::shared_ptr object goes out of scope or is reset.

Here is an example of creating a custom deleter for a smart pointer:

#include <iostream>
#include <memory>

class Person {
public:
    Person(const std::string& name) : name_(name) {
        std::cout << name_ << " created." << std::endl;
    }
    ~Person() {
        std::cout << name_ << " destroyed." << std::endl;
    }
private:
    std::string name_;
};

void custom_deleter(Person* p) {
    std::cout << "Custom deleter called for " << p << std::endl;
    delete p;
}

int main() {
    std::shared_ptr<Person> p1(new Person("Alice"), custom_deleter);
    std::unique_ptr<Person, decltype(&custom_deleter)> p2(new Person("Bob"), &custom_deleter);
    return 0;
}

Output:

Bob destroyed.
Custom deleter called for 0x55f996f4aeb0
Alice destroyed.

In this example, we define a Person class with a custom constructor and destructor that prints a message when the object is created or destroyed. We also define a custom_deleter function that takes a Person pointer as its argument and deletes it.

In main(), we create two smart pointers p1 and p2, one std::shared_ptr and one std::unique_ptr. We pass custom_deleter as the second argument to the std::shared_ptr constructor and as the third argument to the std::unique_ptr constructor.

When the program runs and the main() function returns, both smart pointers will go out of scope and the custom deleter will be called for each one, instead of the default destructor. The custom_deleter function will print a message and delete the Person object that the smart pointer is managing.

Circular referencing

Circular reference with shared pointers occurs when two or more objects hold shared pointers to each other, creating a cycle that prevents the memory from being released properly. This leads to memory leaks and can cause the program to crash if the memory usage becomes too high.

Here’s an example:

class A {
public:
    std::shared_ptr<B> b_ptr;
};

class B {
public:
    std::shared_ptr<A> a_ptr;
};

In this example, objects of class A and B hold shared pointers to each other, creating a circular reference.

To break this cycle, we can use weak_ptr instead of shared_ptr for one of the objects. Since weak_ptr doesn’t increment the reference count, it doesn’t prevent the object from being deleted when no other shared_ptr references exist.

Here’s the modified example:

class A;

class B {
public:
    std::shared_ptr<A> a_ptr;
};

class A {
public:
    std::weak_ptr<B> b_ptr;
};

int main() {
    std::shared_ptr<A> a_ptr(new A);
    std::shared_ptr<B> b_ptr(new B);

    a_ptr->b_ptr = b_ptr;
    b_ptr->a_ptr = a_ptr;

    return 0;
}

In this modified example, class A holds a weak_ptr to class B, which breaks the circular reference and allows the memory to be released properly when the last shared_ptr reference is destroyed.

Exercises

  1. Write a C++ program using smart pointers that creates an integer variable dynamically using new keyword, assign the value to it, print the value using smart pointer.
  2. Write a C++ program using smart pointers that creates a dynamic array of size 5, assigns values to each element of the array, prints each element using smart pointer.
  3. Write a C++ program using smart pointers that creates two integer variables dynamically using new keyword, assigns values to them, adds the values of the variables and prints the result using smart pointer.
  4. Write a C++ program using smart pointers that creates a class named “Rectangle” with member variables “length” and “width”. Create a dynamic object of the class using smart pointer, assign values to the member variables and print them using smart pointer.
  5. Write a C++ program using smart pointers that creates a dynamic object of class “Rectangle” and calculates its area. Use smart pointer functions get(), reset(), and release() to manipulate the object.
  6. Write a C++ program using smart pointers that creates a dynamic object of class “Rectangle” and uses smart pointers to copy its value to a new object. Then use the smart pointer functions to release the old object.
  7. Write a C++ program using smart pointers that creates a dynamic array of size 10 using new keyword, assigns values to each element of the array, prints each element using smart pointer. Then use smart pointer to reset the array to size 5 and print the elements using smart pointer.
  8. Write a C++ program using smart pointers that creates two dynamic objects of class “Rectangle”, assigns values to their member variables, adds their areas and prints the result using smart pointer.
  9. Write a C++ program using smart pointers that creates a dynamic object of class “Rectangle” and calculates its perimeter. Use smart pointer function reset() to update the member variables and calculate the new perimeter. Print both the original and updated perimeters using a smart pointer.
  10. Write a C++ program using smart pointers that creates a dynamic object of class “Rectangle” and calculates its diagonal. Use smart pointer function release() to pass the object to a function that calculates the diagonal and returns it. Print the diagonal using a smart pointer.

Interview questions

  1. What is a smart pointer?
  2. What is the difference between a unique pointer and a shared pointer?
  3. How does a unique pointer ensure that a dynamically allocated object is deleted?
  4. How does a shared pointer ensure that a dynamically allocated object is deleted?
  5. What is a weak pointer?
  6. What is the difference between a weak pointer and a shared pointer?
  7. Can you have a circular reference with shared pointers? If so, how can you break the cycle?
  8. What is the purpose of the get() function in a smart pointer?
  9. What is the purpose of the reset() function in a smart pointer?
  10. What is the purpose of the release() function in a smart pointer?
  11. How can you create a custom deleter for a smart pointer?
  12. What is the purpose of a deleter in a smart pointer?
  13. Can you use a raw pointer as an argument to a smart pointer constructor? If so, why might you do this?
  14. What are some advantages of using smart pointers instead of raw pointers?
  15. What are some disadvantages of using smart pointers?

Leave a Reply

Your email address will not be published. Required fields are marked *

You cannot copy content of this page