14
Oct
2021

Smart Pointers (1/6): std::unique_ptr

If you don’t want to change your raw pointer habits and want to get coverage of smart pointers; then the standard libraries’ unique pointer is the most convenient choice!

Std::unique_ptr exclusively manages the resource during its lifetime.

Here are some features of the unique pointers with proofs and codes!

1. Std::unique_ptrs are the same size as raw pointers unless we use custom deleters.

#include <iostream>
#include <memory>
using namespace std;

int main()
{

  std::unique_ptr<int> uni_ptr(new int(10));

  int *raw_ptr = new int(10);

  cout << "unique_ptr: " << *(uni_ptr) << ", sizeof: " << sizeof(uni_ptr) << endl;

  cout << "raw_ptr: " << *(raw_ptr) << ", sizeof: " << sizeof(raw_ptr) << endl;

  return 0;

}

Here’s the output of the program above:

unique_ptr: 10, sizeof: 8
raw_ptr: 10, sizeof: 8

2. Std::unique_ptrs may be empty. And it has a bool operator overloaded function to check whether there is an object or not.

int main()
{

  std::unique_ptr<int> uni_ptr;

  if (uni_ptr)
    cout << "unique_ptr: " << *(uni_ptr) << endl;
  else
    cout << "unique_ptr has no an object" << endl;

  uni_ptr = std::make_unique<int> (10);

  if (uni_ptr.get())	//the same above
    cout << "unique_ptr: " << *(uni_ptr) << endl;
  else
    cout << "unique_ptr has no an object" << endl;

  return 0;
}

The output of the program above:

unique_ptr has no an object
unique_ptr: 10

3. Std::unique_ptr can’t be copied. Copy mechanism is in conflict with its purpose.

int main()
{

	std::unique_ptr<int> uni_ptr = std::make_unique<int> ();
	std::unique_ptr<int> uni_ptr_2 = uni_ptr;

	return 0;
}

When we try to copy uniqut_ptr here is the compiler issue:

“error: call to implicitly-deleted copy constructor of ‘std::unique_ptr’ ”

4. Std::unique_ptr can be moved.

int main()
{

	std::unique_ptr<int> uni_ptr = std::make_unique<int> ();

	std::unique_ptr<int> uni_ptr_2 = std::move(uni_ptr);

	return 0;
}

the moved unique_ptr (uni_ptr) is deleted.

5. Std::unique_ptr can be constructed with an incomplete type.

The most important point at issue is that in this blog series we have to understand using std::unique_ptr with incomplete types.

Flashback: we used to have raw pointer habits but now we want to use modern technics for std::unique_ptr.

Can we use a raw pointer for incomplete types? Yes… Here is an example:

Suppose we have a class named embeddedWorld. And, we implement a new class type as a data member at the embeddedWorld class (features). And we declare a raw pointer for the implemented class(features *f). Those declarations must be inside the header file (embbeddedWorld.h). The class named Features has not been defined in the header file; therefore, that is known as an incomplete type by the compiler. Everyone is already aware that we should define inside the source file (embeddedWorld.cpp).

embeddedWorld.h and embeddedWorld.cpp files are:

embeddedWorld.h:

#ifndef EMBEDDEDWORLD_H
#define EMBEDDEDWORLD_H

class embeddedWorld
{
	public:
	embeddedWorld();
	~embeddedWorld();

	embeddedWorld(const embeddedWorld &) = delete;
	embeddedWorld &operator=(const embeddedWorld &) = delete;

	private:
		class features;	//forward declaration
	features * f;
};
#endif	// EMBEDDEDWORLD_H[/code]

 

embeddedWorld.cpp:

#include "embeddedworld.h"

class embeddedWorld::features
{
	public: int climate = 0;	//0-> tropical temperate->1
	int population = 0;
};
embeddedWorld::embeddedWorld():
	f(new features) {}

embeddedWorld::~embeddedWorld()
{
	delete f;
}

Everything makes sense! There are no bugs and issues. That works great.

The approach is known as the pimpl (pointer to implementation) idiom.

If we analyze the code superficially we can reach this conclusion:

We defined the separate class in our source file. The implemented class (features) is not an incomplete type anymore at the source file.

We defined a custom destructor to delete our raw pointer.

At the header file, we delete copy semantic functions to prevent the copy mechanism. Because we have a holy terror, raw pointer :). Guess what? The default copy mechanism works and copies the raw pointer to the other embeddedWorld object’s raw pointer. Then if we delete any object, the destructor deletes the resource (raw pointer) in both objects.
Do you see the risk? Either we delete the copy semantic function or we must declare user-defined copy functions to copy deeply.

Now, we can use a raw pointer for incomplete types as std::unique_ptr.

Okay, I don’t want to get sidetracked. We are still talking about std::unique_ptr.

We don’t need to use new and delete keywords if we use smart pointers.

The question is:

If we use the pimpl idiom with std::unique_ptr for incomplete types, we don’t need the destructor to delete our resource! Because unique_ptr deletes the resource when it goes out of scope. What do you think?

It seems logical. But let’s try that! Using the same example above, we are going to change only the raw pointer in the example.

embeddedWorld.h with smart pointer:

#ifndef EMBEDDEDWORLD_H
#define EMBEDDEDWORLD_H
#include "memory"

class embeddedWorld
{
   public:
	embeddedWorld();

   private:
	class features;

	std::unique_ptr<features> feat;

};

#endif	// EMBEDDEDWORLD_H

embeddedWorld.cpp with smart pointer:

#include "embeddedworld.h"

class embeddedWorld::features
{

	public: int climate = 0;	//0-> tropical temperate->1
	int population = 0;

};

embeddedWorld::embeddedWorld(): feat(std::make_unique<features> ()) 
{
    
}

 

And main.cpp to try the example:

#include <iostream>
#include "embeddedworld.h"
using namespace std;

int main()
{

    embeddedWorld myWorld;

    cout << "Hello World!" << endl;

    return 0;
}

At the main function, we only create an embeddedWorld object named myWorld. Everything seems great but if we compile the code we will see this issue as an error: invalid application of ‘sizeof’ to an incomplete type ‘embeddedWorld::features’

If we dive into this error:
we supposed we didn’t need the destructor. Because we didn’t have any resources to delete. That’s why we are using smart pointers. But we have to focus on the unique pointer for the incomplete type that we have. std::unique_ptr has a default deleter to delete its resource. The default destructor is generated by the compiler at the line where the object is created. And we know the default functions generated by the compiler are inline functions. We don’t have the destructor of embeddedWorld class. If we create an object from embeddedWorld the default destructor is generated at the same point. There, embeddedWorld::features is an incomplete type already.

Where the unique_ptr destroys, embeddedWorld::feature must be a complete type. So, embeddedWorld::feature is a complete type inside embeddedWorld.cpp, for this reason, embeddedWorld destructor has to be invoked there, and indirectly unique_ptr default deleter invokes the same point.

In this example, we don’t need the destructor body. We don’t have the code put into it. So, we can define the destructor with a default keyword or empty-body inside the embeddedWorld.cpp

After the implementation of the files:

embeddedWorld.h:

#ifndef EMBEDDEDWORLD_H
#define EMBEDDEDWORLD_H
#include <memory>;

class embeddedWorld
{
	public:
		embeddedWorld();
	~embeddedWorld();	// destructor decleration
	private:
		class features;

	std::unique_ptr<features> feat;

};

#endif	// EMBEDDEDWORLD_H

 

embeddedWorld.cpp with the smart pointer:

#include "embeddedworld.h"

class embeddedWorld::features
{

	public: int climate = 0;	//0-> tropical, temperate->1
	int population = 0;

};

embeddedWorld::embeddedWorld(): feat(std::make_unique<features> ()) {}
embeddedWorld::~embeddedWorld() = default;	// or {}

There is no issue.

But we should remember the rule of 0/3/5 if we need other semantics (copy or move). Because we used custom destructor.

6. Std::unique_ptr has non-member functions to create new unique pointer: std::make_uniqe

The function, Std::make_unique uses the perfect-forwarding method.

We can see how to create a std::unique_ptr in three ways, below.

std::unique_ptr<int> uni_ptr ( new int(10));

std::unique_ptr<int> uni_ptr = std::make_unique<int>(10);

auto uni_ptr(std::make_unique<int>(10));

If we use a braced initializer for the make_unique parameter, the perfect-forwarding method won’t run. Because the perfect-forwarding mechanism can’t forward braced initializers.

The other significant matter is the creation of memory leakage to use a new keyword with std::unique_ptr, if some sequential commands work and if an exception occurs!

Suppose we have a function, it takes two parameters: unique_ptr and an int value. Its type or value is not necessary. The parameter, value is provided from the generator function that may throw an exception.

For an examination of this situation, the causal code:

#include <iostream>
#include "memory"
using namespace std;

void *operator new(size_t size)
{

	void *p = std::malloc(size);

	cout << "in the new operator overloaded function" << endl;

	return p;
}

int createSomeIntValue()
{
	//somecode
	throw 3;

	return 5;
}

void foo(std::unique_ptr<int> ptr, int value)
{

	cout << "inside the function, foo" << endl;
	cout << "parameter value" << value << endl;

	cout << ptr.get() << endl;
	//some code
}

int main()
{

	try
	{
		foo(std::unique_ptr<int> (new int(5)), createSomeIntValue());
	}
	catch (int ex)
	{
		cout << "We caught an exception with value:" << endl;
	}

	return 0;
}

 

When the function foo executes, the parameter, created new int(5) must be executed for the constructor of unique_ptr. We certainly know that. If we want to observe it we can overload the new operator like the code above:

What about the other execution order for parameters? We can’t be sure about that.

Please read this article: https://isocpp.org/blog/2016/08/quick-q-why-doesnt-cpp-have-a-specified-order-for-evaluating-function-argum

We will focus on this order:

After the line, new int(5) executes, the function createSomeIntValue may be executed. If it throws an exception the order is being broken and the unique_ptr constructor will not be executed. Who manages the created new int(5)? No one!

This is a memory leakage problem.

For avoiding this problem we disuse the new keyword. The function, make_unique comes to the rescue us!

foo(std::make_unique<int>(5), createSomeIntValue());

While using the make_unique function to generate a new unique pointer with the resource, the order of evaluating function arguments will be insignificant.

make_unique or createSomeIntValue will be executed, the memory leakage issue won’t occur. If the first, make_unique will be executed, and, then, if createSomeIntValue throws an exception that will be executed, the unique_ptr destructor will handle the resource. Using make_unique always pushes us to be on the safe side!

We can use the same idiom while using the shared_ptr.

Okay, In the next article we can create our unique_ptr to understand smart pointers comprehensively.

The next article: Smart Pointer-2: How to Create Our Own Unique Pointer

à bientôt

Resources:
std::unique_ptr
std::make_unique
Effective Modern C++ by Scott Meyers