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 call delete). If the pointer returned by new 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:

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:

Found in https://github.com/opencv/opencv/blob/68d15fc62edad980f1ffa15ee478438335f39cc3/modules/gapi/src/compiler/passes/transformations.cpp

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.