Search⌘ K
AI Features

Smart Pointers as Pointers

Explore how to apply const with smart pointers in C++. Understand the difference between const pointers and const pointed values, learn the implications for pointer resets and modifications, and discover best practices for using const with std::unique_ptr to improve code safety.

There are two ways to approach smart pointers. One way is to consider that smart pointers are effective pointers. Either the pointer or the pointed value can be const. Or both.

In another perspective, we consider that smart pointers are class-type objects. Basically, they are wrapping pointers. As a smart pointer is an object, the rule of thumb says that it can be passed around as a const reference. We are going to see that in most cases, it’s a bad idea.

Let’s examine both perspectives.

const and smart pointers as pointers

Smart pointers are effective pointers because they clean up after themselves. Therefore, we can use const both with the pointer itself or the pointed value.

There are different smart pointers, but as our default choice should be std::unique_ptr, we’ll use this one throughout our examples. Except when we explicitly need a shared pointer to demonstrate specific concepts.

const std::unique_ptr<T>

In this case, it’s the pointer that is const and not what we point to. It means that we cannot reset the pointer or change what it points to. At the same time, the value it points to can be modified.

C++
#include <iostream>
#include <memory>
int main() {
const std::unique_ptr<int> p = std::make_unique<int>(42);
++(*p); // OK, data is not const
// p.reset(new int{666}); // ERROR cannot reset const pointer
std:: cout<<*p<<'\n';
}

It’s worth noting that a const unique_ptr provides limited options. We cannot return it because it requires moving away from the pointer. Therefore, the following code will not work.

C++
#include <iostream>
#include <memory>
using namespace std;
const std::unique_ptr<int> f() {
const std::unique_ptr<int> p = std::make_unique<int>(42);
return p;
}

Meanwhile, a const return type is not a problem starting from C++17. The snippet below produces the same error on C++14 as the above one. However, it works with C++17:

C++
#include <iostream>
#include <memory>
const std::unique_ptr<int> f() {
std::unique_ptr<int> p = std::make_unique<int>(42);
return p;
}
int main() {
}

The difference between the two snippets is that the p is not declared const.

std::unique_ptr<const T>

In this case, the value the pointer points to is const, but the pointer itself is mutable. In other words, we cannot change the value of the pointed data, but we can change what the pointer points to.

C++
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<const int> p = std::make_unique<const int>(42);
// ++(*p); // ERROR, data is const
p.reset(new int{51}); // OK, pointer is not const
std::cout << "Successful\n";
}

In the expression std::make_unique<const int>(42) the const is not mandatory, the code will compile even if we forget the const. However, it’s best to remember it. If we check it in compiler explorer, missing the const results in an extra move constructor call, we have to destruct the temporary object:

C++
mov DWORD PTR [rbp-20], 42
lea rax, [rbp-32]
lea rdx, [rbp-20]
mov rsi, rdx
mov rdi, rax
call std::_MakeUniq<int>::__single_object std::make_unique<int, int>(int&&)
lea rdx, [rbp-32]
lea rax, [rbp-40]
mov rsi, rdx
mov rdi, rax
call std::unique_ptr<int const, std::default_delete<int const> >::unique_ptr<int, std::default_delete<int>, void>(std::unique_ptr<int, std::default_delete<int> >&&)
lea rax, [rbp-32]
mov rdi, rax
call std::unique_ptr<int, std::default_delete<int> >::~unique_ptr() [complete object destructor]

In case we don’t forget the const within std::make_unique, the above code simplifies to:

C++
mov DWORD PTR [rbp-36], 42
lea rax, [rbp-48]
lea rdx, [rbp-36]
mov rsi, rdx
mov rdi, rax
call std::_MakeUniq<int const>::__single_object std::make_unique<int const, int>(int&&)

To summarize, if we want a const smart pointer, use const both on the left and the right side, given that you use the std::make_* functions. It’s often better to use auto.

const std::unique_ptr<const T>

In this case, it’s a combination of the two consts. Both the pointed value and the (smart) pointer are const; therefore, no change is accepted.

C++
#include <iostream>
#include <memory>
int main() {
const std::unique_ptr<const int> p = std::make_unique<const int>(42);
//++(*p); // ERROR, data is const
//p.reset(new int{51}); // ERROR, pointer is const
}

Remember to use the const on both sides!