Last Update:
6 More Ways to Refactor new/delete into unique ptr
Table of Contents
In the first part of our refactoring series, we covered (smart) pointers inside a function body; today, I’d like to show you cases for return types, data members, and a few others.
Let’s jump in and replace some new()
and delete
!
See the first part
This article is the second in the series about refactoring with unique_ptr
. You can read the first part here:
6 Ways to Refactor new/delete into unique ptr - C++ Stories
Basic assumptions
The code presented here will usually contain explicit new
and delete
, and we’d like to reduce their use and wrap resources (pointers) into RAII.
We’ll try to follow the rule: R.11: Avoid calling new and delete explicitly from the Core C++ Guidelines:
The pointer returned by
new
should belong to a resource handle (that can calldelete
). If the pointer returned bynew
is assigned to a plain/naked pointer, the object can be leaked.
Ok, enough for the background, let’s start with the first element.
1. Returning a pointer from a factory function
Factory functions create objects, sometimes perform additional initialization, setup and then return the object to the caller:
MyObject* BuildObject(int param) {
MyObject *p = new MyObject();
p->setParam(param);
// initialize...
return p;
}
auto ptr = BuildObject(100);
The above code shows a simplified factory function.
What should happen with ptr
?
Do you own it? Should you delete
it?
We see the code of BuildObject,
so it’s clear for us, but if you can access only the header file and there’s no comment, you might not be aware that the function also implies the ownership for the returned object.
To avoid confusion, It’s best to make the API explicit:
std::unique_ptr<MyObject> BuildObject(int param) {
auto p = std::make_unique<MyObject>();
p->setParam(param);
// initialize...
return p;
}
Now, the user knows that the ownership is passed. What’s more, there’s nothing to do in most cases, as the memory will be released appropriately.
While the function returns unique_ptr
, there’s no big deal if we want to convert it into a shared_ptr
.
Have a look:
std::shared_ptr<MyObject> shared = BuildObject(100);
The shared_ptr
class has a constructor that takes unique_ptr
and takes its owned pointer, and also wraps the deleter properly.
See the code @Compiler Explorer.
Creating Storage Example
Let’s have a look at some real code. This time it will be a good example from OpenCV:
A bit simplified version from opencv/gfluidbuffer.cpp:
std::unique_ptr<fluid::BufferStorage>
createStorage(int capacity, int desc_width, int type,
int border_size, fluid::BorderOpt border)
{
if (border)
{
std::unique_ptr<fluid::BufferStorageWithBorder>
storage(new BufferStorageWithBorder);
storage->init(type, border_size, border.value());
storage->create(capacity, desc_width, type);
return storage;
}
std::unique_ptr<BufferStorageWithoutBorder>
storage(new BufferStorageWithoutBorder);
storage->create(capacity, desc_width, type);
return storage;
}
Inside createBuffer
, we check for the input params. Based on the border
value, the function selects the final BufferStorage
class and creates unique_ptr
from it.
2. As a class member
Let’s consider a case where you have an owning raw pointer as a class member:
struct Data {
int i { 0 };
double j {0.0};
};
class Package {
public:
Package(std::string name, double m)
: name_(std::move(name))
, mass_(m)
, extra_(m > 100.0 ? new Data{} : nullptr )
{
}
~Package() noexcept { if (extra_) delete extra_; }
// copy/move!
Package(const Package& other)
: name_(other.name_)
, mass_(other.mass_)
, extra_(other.extra_ ? new Data(*(other.extra_)) : nullptr)
{
}
Package(Package&& other) noexcept
: name_(std::move(other.name_))
, mass_(other.mass_)
, extra_(other.extra_)
{
other.extra_ = nullptr;
}
Package& operator=(const Package& other) {
name_ = other.name_;
mass_ = other.mass_;
extra_ = other.extra_ ? new Data(*(other.extra_)) : nullptr;
return *this;
}
Package& operator=(Package&& other) noexcept {
name_ = std::move(other.name_);
mass_ = other.mass_;
std::swap(extra_, other.extra_);
return *this;
}
private:
std::string name_;
double mass_ { 0.0};
Data* extra_ { nullptr };
};
See code @Compiler Explorer
Inside the Package
class, I have a pointer data member that might be optionally initialized with some extra Data
object.
There’s so much code that needs to handle it correctly:
- Manual allocation in a constructor
- Deallocation in destructor
- Handling of the rule of five - all special member functions have to be added
- Safety - during construction, when the pointer is already created, but then the parent class’s constructor throws, you might get leaks as the destructor won’t be called. If you have smart pointers, then the code is properly cleaned up. See this question: c++ - Is the destructor called if the constructor throws an exception? - Stack Overflow
How to solve those issues?
Since the object is optional, then you can also leverage std::optional
. But let’s say you want to save some space in your class for that extra data (if it’s rarely created). Then you can stick to unique_ptr
:
Here’s the modified version with unique_ptr
@Compiler Explorer.
We also have to implement special functions in this version, but now the code is a bit shorter and much safer. No leaks are possible.
Pimpl
Having a pointer inside a class also makes it possible to hide the implementation details from the type declaration. Such a technique is called PIMPL - Pointer to IMPLementation. It’s often used to reduce dependencies and improve compilation times.
You can read more about this patter in my other articles:
- The Pimpl Pattern - what you should know - C++ Stories
- pimpl vs Abstract Interface - a practical tutorial - C++ Stories
3. Building a container of pointers
Have a look at the code I found in pcmanager repository (code from 2008 if I look correctly): https://searchcode.com/file/5316275/src/import/kpfw/netpop.h/
class KFindVirusInfoVec: public IKPopData
{
private:
vector<KFindVirusInfo*> m_VirusInfo;
public:
KFindVirusInfoVec() {}
~KFindVirusInfoVec() { Clear(); }
// ...
void AddInfo(LPCWSTR file, LPCWSTR desc)
{
KFindVirusInfo* pInfo = new KFindVirusInfo(file, desc);
m_VirusInfo.push_back(pInfo);
}
private:
void Clear()
{
for (int i = 0; i < (int)m_VirusInfo.size(); i++)
delete m_VirusInfo[i];
m_VirusInfo.clear();
}
Play with simplified version @Compiler Explorer
As you can see, we have a vector of pointers, and it’s managed manually.
Let’s assume that we need pointers in the container, and we cannot change it into value-type semantics. For example, AddInfo
could add some other type, derived from KFindVirusInfo
.
What’s more, try the following use case:
KFindVirusInfoVec viruses;
viruses.AddInfo("grizzly");
viruses.AddInfo("bug #165X");
KFindVirusInfoVec other = viruses; // << ??
When I executed this code under Compiler Explorer, I got the following:
Program returned: 139
free(): double free detected in tcache 2
It’s because the special member functions for KFindVirusInfoVec
weren’t implemented, and all we get by default is a shallow copy.
The best thing that we could do here is to use a smart pointer as it nicely fits into a vector.
vector<unique_ptr<KFindVirusInfo>> m_VirusInfo;
And now:
- there’s no need for
Clear()
- as we’ll have a nice cleanup in the destructor, automatically. - we have regular pointer semantics, so other derived types from
KFindVirusInfo
can be held there. - important!: since
unique_ptr
is a moveable type only, the compiler will warn us about attempts to copy the whole parent object.
Here’s the modified version @Compiler Explorer
What’s more, if you attempt to copy, then you’ll get some nasty compiler errors, and this will force you to implement (or think about) if the whole type is copyable or not.
4. Passing a pointer with the ownership
If you have a function:
void func(T* ptr) {
}
It’s not clear to decipher what it might do to the pointer.
In Modern C++, we treat all raw pointers as non-owning only. But in legacy code, this function can even call delete ptr
!
That’s why according to this guideline:
R.32: Take a
unique_ptr<widget>
parameter to express that a function assumes ownership of a widget
Have a look at the example:
struct Package {
~Package() { std::cout << fmt::format("{} dtor\n", name_); }
std::string name_;
double price_ { 0.0};
};
void consumePackage(std::unique_ptr<Package> pack) {
if (pack) {
std::cout << fmt::format("{}, price: {}\n",
pack->name_, pack->price_);
}
}
int main() {
auto pack = std::make_unique<Package>("C++ book", 29.99);
consumePackage(std::move(pack));
std::cout << "back in main()\n";
return 0;
}
Play with code @Compiler Explorer
The output:
C++ book, price: 29.99
C++ book dtor
back in main()
In this simplified example, pack
is passed to consumePackage
, and it’s destroyed at the end of that function. When we’re back in main()
, the pointer is not valid anymore.
Have a look at my separate blog post about sink function: Modernize: Sink Functions - C++ Stories.
5. Observing pointers only
What if you want to observe the pointer only?
In this case, the C++ Core Guidelines suggest passing T*
. That’s quite clear if we assume that all raw pointers are non-owning.
I’ve also seen a lot of cases where a function takes const unique_ptr<T>& ptr
, like here:
bool tryToSubstitute(ade::Graph& main,
const std::unique_ptr<ade::Graph>& patternG,
const cv::GComputation& substitute)
{
GModel::Graph gm(main);
// 1. find a pattern in main graph
auto match1 = findMatches(*patternG, gm);
if (!match1.ok()) {
return false;
}
// 2. build substitute graph inside the main graph
// ...
const Protocol& patternP = GModel::Graph(*patternG).metadata().get<Protocol>();
// 3. ...
checkCompatibility(*patternG, gm, patternP, substituteP);
// 4. make substitution
performSubstitution(gm, patternP, substituteP, match1);
return true;
}
In the above function you can see that it takes const std::unique_ptr<ade::Graph>& patternG
. But since it’s a constant reference, you won’t be able to change it. It has the same effect as passing ade::Graph*
.
Which one is better for you? Passing just a pointer or const unique_ptr&
?
The latter is quite lengthy and might confuse the reader: do we pass unique_ptr here? Ah no… I cannot change it anyway. So, in my opinion, it’s best to stick to the Core C++ Guideline.
If you're interested in smart pointers - have a look at my handy reference card. It covers everything you need to know about unique_ptr
, shared_ptr
and weak_ptr
, wrapped in a beautiful PDF:
6. Updating the pointer
I’m always confused with the following code:
void foo(T** pp) {
*pp = new T;
}
T *ptr = nullptr;
foo(&ptr);
What if ptr
was already allocated? Then the foo
function has to release it before a new allocation happens.
And how about this case:
void foo(unique_ptr<T>& pp) {
pp = std::make_unique<T>();
}
In the second case, we pass unique_ptr
by non-const reference, so it’s clearer that the function might modify it. Moreover, there’s no need to handle special cases with non-null pointers as the unique_ptr
class covers everything.
This follows the rule: R.33:
R.33: Take a unique_ptr& parameter to express that a function reseats thewidget
Additionally in C++23, you can have a look at out_ptr
helper type that allows interaction between smart pointers and C-style functions taking pointers to pointers. See at [Cppreference](std::out_ptr - cppreference.com).
Summary
In this article, we focused on interactions between several functions and also storing pointers inside classes.
We explored various issues with raw pointers:
- safety when creating inside constructors (smart pointers can release the memory even if the constructor of the parent object throws!)
- readability - when you have a raw pointer, it’s not clear if it owns the object or not. In Modern C++, we should follow the rule that a raw pointer is always non-owning.
- making code longer - with raw pointers and explicit memory management handling, you have many cases to cover.
Once again, have a look at the first part of this article: 6 Ways to Refactor new/delete into unique ptr - C++ Stories
Back to you
- How do you use smart pointers?
- What’s your everyday use case to refactor into smart pointers?
Share your feedback in the comments below the article.