Table of Contents

In legacy code, you can often spot explicit new and delete lurking in various places and waiting to produce pointer-related issues. This blog post shows six patterns to improve that erroneous style and rely on modern techniques, especially unique_ptr and other helper standard types.

Bonus point 1: if possible, we’ll also look at some existing code from open source projects!
Bonus point 2: this article is just part of the story! Next time you’ll see additional six topics related to refactoring with unique_ptr. Seee the blog post here.

Basic assumptions  

The code presented here will usually contain explicit new and delete, and we’d like to reduce their use and wrap resources (usually for pointers to heap-allocated objects) into RAII.

Most importantly, 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.

The code presented here can be refactored in many different ways; I only focus on the smart pointer cases.

1. Just use the stack  

The best advice for pointers and smart pointers:

If you can, don’t use them :)

Smart pointers are great… but if you can rely on the stack, then please use it.

Let’s have a look at some code legacy function with explicit new:

void someLongFunction() {
    // variables...
    Object *pObj = new Object();
        
    // processing
    
    if (/*event_one*/) {
        delete pObj;
        return;
    }
    
    if (/*event_two*/) {
        delete pObj;
        return;
    }
    
    delete pObj;
}

Yeah… While the code above is very generic and made up, I’ve seen similar code in real projects :) What’s more, someone can easily forget that delete when returning early from such functions.

In many cases you can just do this:

void someLongFunction() {
    // variables...
    Object obj;
    
    // processing
    if (/*event_one*/)
        return;
    
    if (/*event_two*/)
        return;

}	// no need to delete anything... it's automatic storage duration...

As you can see, the code is much simpler now, and there’s no need to manage memory explicitly.

The default stack size is around 1MB in MSVC (platform-specific), including memory for the call stack params. In many cases, the size is large enough to hold most of your objects.

What’s more, if you have objects like std::vector or std::string, they will eat only a tiny fraction of the stack, as they use the heap to store the buffer for the elements. And what’s best is that they manage memory themselves, so it’s completely invisible to you.

2. Locally for large objects  

But sometimes, if your object is large, or you know you can fill the stack quickly (some recursion or a deep level of the call stack), then you need to create the object on the heap:

In that case it’s best to rely on unique_ptr and then don’t bother with the need to delete the memory:

void someLongFunction() {
    // variables...
    auto pObj = std::make_unique<Object>(); // pointer
    
    // processing
} // no need to delete explicitly

The main advantage is that the pointer will go out of scope and then invoke the delete operator.

This can save us if you have early returns from a function:

void someLongFunction() {
    // variables...
    auto pObj = std::make_unique<Object>(); // a pointer
    
    // processing
    if (/*event_one*/)
        return;
    
    if (/*event_two*/)
        return;
    
    // other code...
} // no need to delete explicitly

Master Playlist test example  

When looking at some real-life projects, I’ve found this in VideLan repository: M3U8.cpp

int M3U8MasterPlaylist_test() {
    vlc_object_t *obj = static_cast<vlc_object_t*>(nullptr);

    const char manifest0[] = /* */

    M3U8 *m3u = ParseM3U8(obj, manifest0, sizeof(manifest0));
    try {
        // some calls on m3u
        delete m3u;
    }
    catch(...)  {
        delete m3u;
        return 1;
    }

    const char manifest1[] = /* */

    m3u = ParseM3U8(obj, manifest1, sizeof(manifest1));
    try {
        // some calls on m3u
        delete m3u;
    }
    catch(...) {
        delete m3u;
        return 1;
    }

    return 0;
}

The above file is just some test code, but as you can notice, the author had to remember about calls to delete m3u in several places. It would be much easier to wrap the pointer into a unique_ptr and not bother with delete.

3. Locally for polymorphic objects  

Another use case might be when you don’t know the exact object type and rely on the polymorphism.

Let’s create the following simple hierarchy:

struct BaseObject {
    virtual ~BaseObject() = default;

    virtual double calcArea() const noexcept = 0;
};

struct Rectangle : BaseObject {
    Rectangle(double a, double b) : a_(a), b_(b) { }

    double calcArea() const noexcept override { return a_ * b_; }

    double a_ { 0.0};
    double b_ { 0.0};
};

And then use it:

int getUserInput() { return /* */ }
void display(std::string_view title, double val) { /* ... */ }

BaseObject* createShape(int userInput) {
    if (userInput > 0)
        return new Rectangle(10, 5);

    return nullptr;
}

void processShape() {
	BaseObject* pObj = createShape(getUserInput());
    
    if (pObj) {
	    const auto area = pObj->calcArea();
        display("area is", area);
    }
    
    delete pObj;
}

The example shows a basic polymorphism with shape classes. In processShape, we don’t know the final type of the object; we base it on the getUserInput().

In this case, you have several options:

  • wait for the next article in the series (soon) go to point 7 and fix the return type of the function (if you can change its API)
  • or at least wrap the returned pointer into a unique_ptr.

See below:

void processShape() {
	std::unique_ptr<BaseObject> pObj { createShape(getUserInput()) };
    
    if (pObj) {
	    const auto area = pObj->calcArea();
        display("area is", area);
    }
}

Play with code @Compiler Explorer

As you can see, unique_ptr can handle pointers to the base class so that you can use it naturally.

4. Locally for optional objects  

I’ve also seen a use case where the object might be optionally created:

struct Package {
    std::string name_;
    double mass_ { 0.0};
};

void display(std::string_view title, auto val) {
    std::cout << fmt::format("{} {}\n", title, val); 
               // ^ replace with std::format in C++20
}

double tryMassReduction(double mass) { return mass*0.95; }

constexpr double maxMass = 100.0;

void processPackage(const Package& pack) {
	Package* temp = nullptr;

    if (pack.mass_ > maxMass) {
        temp = new Package(pack);
    }

    display("initial mass:", pack.mass_);

    if (temp) {
        temp->mass_ = tryMassReduction(temp->mass_);
        display("after reduction:", temp->mass_);
    }

    delete temp;
}

Play with code @Compiler Explorer

The above example shows an optional temp object that might be created when the given package mass is larger than maxMass.

How to refactor it and get rid of new?

We have at least two options:

  • Use std::optional.
  • Use unique_ptr.

Let’s try with std::optional:

void processPackage(const Package& pack) {
	std::optional<Package> temp;

    if (pack.mass_ > maxMass) {
        temp = Package(pack);
    }

    display("initial mass:", pack.mass_);

    if (temp) {
        temp->mass_ = tryMassReduction(temp->mass_);
        display("after reduction:", temp->mass_);        
    }
}

Play with code @Compiler Explorer

As you can see, std::optional is more readable and has pointer semantics so that you can use it similarly to a pointer. It has the -> operator overloaded, and you can check if the object is valid with a boolean conversion operator.

Here are the pros and cons of the approach:

  • memory can be wasted for optional and when the object is not needed.
  • optional uses the same amount of memory, and there won’t be extra memory allocations
  • with unique_ptr you might need extra allocation
  • it’s hard to pass unique_ptr furher to other functions, optional is more natural as a parameter

You can find more in:

And my separate article on optional: Using C++17 std::optional - C++ Stories.

5. Locally for buffers  

In many cases the new operator is handy for memory buffers:

char* buf = new char [runtime_size];
// use buf...
delete [] buf;

In this case, we also have several options to reduce the use of new:

  • std::unique_ptr to store the data,
  • use a standard container

To consider some real-life scenarios, let’s have a look at some basic code that loads all data from a file:

We can use std::string:

const auto fsize = fs::file_size(name);
std::string str(static_cast<size_t>(fsize), 0);
inFile.read(str.data(), str.size());

Play with code @Compiler Explorer

Or unique_ptr

const auto fsize = static_cast<size_t>(fs::file_size(name)); 
                  // ^ assume no conversion errors...

auto buf = std::make_unique<char[]>(fsize+1);

inFile.read(buf.get(), fsize);
if (!inFile)
    throw std::runtime_error("Could not read from " + name.string());

display("file contents:", buf.get());
display("last letter (value):", static_cast<int>(buf[fsize]));

Play with code @Compiler Explorer

As you can see, it was much easier with the appropriate container, as I didn’t have to mess with the last marker at the end of the string.

An array of floats  

Here’s another example, from some real-life code, I found in: Collada DAE2OgreSerializer.cpp:

void OgreSerializer::writeFloats(const double* const pDouble, size_t count) 
{
    // Convert to float, then write
    float * tmp = new float[ count ];

    for ( unsigned int i = 0; i < count; ++i ) {
        tmp[ i ] = static_cast<float>( pDouble[ i ] );
    }

    if ( mFlipEndian )  {
        flipToLittleEndian( tmp, sizeof( float ), count);
        writeData( tmp, sizeof( float ), count );
    }

    else {
        writeData( tmp, sizeof( float ), count );
    }

    delete [] tmp;
}

In the above code, tmp is created as the array of floats of the size count, which is passed as an argument to the function.

In the above case we can replace it with:

auto tmp = std::make_unique<float[]>(count);

If we want to don’t waste time on default initialization of the buffer (make_unique for arrays sets values o 0.0f on all elements), we could use make_unique_for_overwrite from C++20:

auto tmp = std::make_unique_for_overwrite<float[]>(count);

And now, the tmp has unspecified values for elements.

Would you like to see more?
The _for_overwrite functions allow for even 20x init speed up! See my premium article with a benchmark which is available for C++ Stories Premium/Patreon members. See all Premium benefits here.

6. Inside a function, multiple types  

Here is one of the main benefits of smart pointers.

Since they hold the ownership, they will ensure the data is released at the end of the function.

That can save us in a situation where you have several pointers:

void processPackage(const Package& pack) {
	Package* temp = nullptr;
    Package* anotherTemp = nullptr;

    if (pack.mass_ > maxMass) {
        temp = new Package(pack);
    }

    if (std::empty(pack.name_)) {
        delete temp;
        return;
    }

    if (pack.mass_ > maxMass*2) {
        anotherTemp = new Package(pack);
    }

    display("initial mass:", pack.mass_);

    if (pack.name_ == "Special Box") {
        delete temp;
        delete anotherTemp;
        return;
    }

    if (temp) {
        temp->mass_ = tryMassReduction(temp->mass_);
        display("after reduction:", temp->mass_);
    }

    delete temp;
    delete anotherTemp;
}

Play with code @Compiler Explorer

We have two early returns, and each time we need to make sure we delete the objects! Not to mention, the code can throw an exception from a function or memory allocation.

If you replace code with unique_ptr, the code is more straightforward; you can just return and don’t bother with leaks.

Real code in VLC:  

Let’s have a look at the following code from VLC: adaptive.cpp:

static PlaylistManager * HandleHLS(demux_t *p_demux,
                                   const std::string & playlisturl,
                                   AbstractAdaptationLogic::LogicType logic)
{
    SharedResources *resources =
          SharedResources::createDefault(VLC_OBJECT(p_demux), playlisturl);
    if(!resources)
        return nullptr;

    M3U8Parser parser(resources);
    M3U8 *p_playlist 
       = parser.parse(VLC_OBJECT(p_demux),p_demux->s, playlisturl);
    if(!p_playlist)
    {
        msg_Err( p_demux, "Could not parse playlist" );
        delete resources;
        return nullptr;
    }

    HLSStreamFactory *factory = new (std::nothrow) HLSStreamFactory;
    HLSManager *manager = nullptr;
    if(!factory ||
       !(manager = new (std::nothrow) HLSManager(p_demux, resources,
                                         p_playlist, factory, logic)))
    {
        delete p_playlist;
        delete factory;
        delete resources;
    }
    return manager;
}

As you can see, the heap allocation happens with no throw; while make_unique doesn’t have a way to specify that, we can still wrap the pointer into a smart pointer:

std::unique_ptr<T> p = new(std::nothrow) T();

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:

Summary  

I hope you stayed with me and followed those six important examples of refactoring. We went from wrapping a single pointer to multiple objects and even to holding whole dynamic buffers.

As you could see in several places, I even discouraged using smart pointers as there are better alternatives: most likely a simple object on the stack or a standard container.

And that’s not all!

In the next part of the article, we’ll discuss more cases like having a pointer as a data member, passing to sink functions, returning smart pointers from a factory function. See it here: 6 More Ways to Refactor new/delete into unique ptr - C++ Stories

Your turn

  • How do you use smart pointers?
  • What’s your common use case to refactor into smart pointers?

Share your feedback in the comments below the article.