Dec 17, 2019

Avoiding Common Problems with Smart Pointers

Introduction

Incorrectly passing allocators when creating or resetting smart pointers (bslma::ManagedPtr and bsl::shared_ptr) is the most common mistake made when using those pointers.

We have added checks to bde_verify to detect patterns of misuse and we have created utility methods to encapsulate passing allocators properly when creating smart pointer objects.

We recommend using the utility methods

bslma::ManagedPtrUtil::makeManaged
bslma::ManagedPtrUtil::allocateManaged
bsl::make_shared
bsl::allocate_shared

to create smart pointer objects instead of doing so “by hand”.

Creating a Managed Pointer

In full-blown usage, the creation of a managed pointer can involve three mentions of an allocator. A typical such case might be -

void foo(bslma::Allocator *theAllocator)
{
    // ...
    bslma::ManagedPtr<bsl::string> smp(       // 0
        new (*theAllocator)                   // 1
        bsl::string(100, 'x', theAllocator),  // 2
        theAllocator);                        // 3
    // ...
}

We discuss each appearance of the allocator below in understanding smart pointer construction.

Possibilities for Error

Whenever the same thing needs to be repeated multiple times, there is always the possibility of error.

In the case of managed pointer usage, all three allocator arguments are optional, so erroneous usage may include leaving out arguments that should have been supplied, causing the default allocator instead of the supplied allocator to be used. And since the supplied allocator very often is the default allocator, such misuse can go unnoticed for a long time.

bde_verify Checks

As of version 1.3.12, bde_verify contains a check named managed-pointer which attempts to identify possible errors in smart pointer construction (for both ManagedPtr and shared_ptr, despite the name of the check) and reset. They’re described in more detail below, but in summary, the’re tagged MP01, MP02, … and they detect mismatched allocator use.

To run only this check, use

    bde_verify -cl='all off' -cl='check managed-pointer on' files...

See the bde_verify documentation for more detail.

Avoid Errors by Using Utilities

BDE 3.44 provides new utility methods for creating managed pointers. Similar utility methods for shared pointers previously existed. These methods encapsulate object creation and the passing of allocators so that the allocator argument does not need to be repeated, The methods come in pairs, a make variant that uses the default allocator and an allocate variant that takes a specified allocator.

The example above becomes -

    bslma::ManagedPtr<bsl::string> smp =
        bslma::ManagedPtrUtil::allocateManaged<bsl::string>(
            theAllocator, 100, 'x');

If we were creating a shared pointer instead of a managed pointer, we would say -

    bsl::shared_ptr<bsl::string> ssp =
        bsl::allocate_shared<bsl::string>(theAllocator, 100, 'x');

Note that we pass the allocator first, followed by the arguments to the object constructor. The utility functions query traits of the object type to determine whether the object uses allocators, and if so, forward the allocator to the constructor of the object as the last argument.

If we want to use the default allocator, we can say -

    bslma::ManagedPtr<bsl::string> smp =
        bslma::ManagedPtrUtil::makeManaged<bsl::string>(100, 'x');
    bsl::shared_ptr<bsl::string> ssp = bsl::make_shared<bsl::string>(100, 'x');

Understanding Smart Pointer Construction and Its Discontents

Creating Managed and Shared Pointers

Here is the code for smart pointer creation, once again -

void foo(bslma::Allocator *theAllocator)
{
    // ...
    bslma::ManagedPtr<bsl::string> smp(            // 0
        new (*theAllocator)                        // 1
        bsl::string(100, 'x', theAllocator),       // 2
        theAllocator);                             // 3
    // ...
    bsl::shared_ptr<bsl::string> ssp(              // 0
        new (*theAllocator)                        // 1
        bsl::string(100, 'x', theAllocator),       // 2
        theAllocator);                             // 3
    // ...
}

Resetting Managed and Shared Pointers

Resetting a managed pointer is similar -

void bar(bslma::ManagedPtr<bsl::string>&  smp,
         bsl::shared_ptr<bsl::string>&    ssp,
         baslma::Allocator               *theAllocator)
{
    // ...
    smp.load(new (*theAllocator)                    // 1
             bsl::string(100, 'x', theAllocator),   // 2
             theAllocator);                         // 3
    // ...
    ssp.reset(new (*theAllocator)                   // 1
              bsl::string(100, 'x', theAllocator),  // 2
              theAllocator);                        // 3
    // ...
}

The Allocator Arguments

At line 0 we create a smart pointer to string.

At line 1, we issue the new expression to allocate the string that will be held by the managed pointer, passing it the placement argument (*theAllocator) so that it will use the specified allocator to allocate memory for the string.

At line 2, we pass the allocator to the string constructor so that the string will use that allocator for its internal memory allocation; the fact that the string itself was allocated using a particular allocator does not automatically forward that allocator to the string. Without this argument, the string would use the default allocator for its internal allocations.

Finally, at line 3 we pass the allocator to the managed pointer constructor to use as the means for deallocating the held string. Deallocation must match allocation; since memory for the held string came from theAllocator (at line 1), we must use theAllocator to free that memory.

Consequences of Omission or Mismatch

Leaving out the allocator at line 2, or using a different one, is the least pernicious. It results in the string using a different allocator from the one used to allocate the string. This is not an error per se, but it may lead to inefficiency by failing to take advantage of the presumably more performant supplied allocator.

Leaving out the placement argument at line 1 or the deleter argument at line 3 is the most troublesome, since it implies that the wrong method will be used to deallocate memory. However, the default allocator, unless reset, allocates and frees memory using operator new and operator delete, so omitting one of line 1 and line 3 can still result in code that “works” until a non-default allocator is supplied.

And obviously, actual mismatches between line 1 and line 3 will cause errors if the deleter attempts to free memory in a way inconsistent with how the allocator supplied it.

Details of the bde_verify managed-ptr Check

Examples of the output it produces follow -

file.cpp:28:22: warning: MP01: Shared pointer without deleter
      will use 'operator delete'
    ManagedPtr<char> mp02(new (*pa) char);
                     ^    ~~~~~~~~~~~~~~

file.cpp:72:22: warning: MP02: Different allocator and deleter
      for shared pointer
    ManagedPtr<char> ep03(new (ta) char, ba);
                     ^         ~~        ~~

file.cpp:70:22: warning: MP03: Deleter provided for non-placement
      allocation for shared pointer
    ManagedPtr<char> ep01(new char, da);
                     ^    ~~~~~~~~  ~~

bde_verify attempts to detect if an allocator variable has been initialized to the default allocator explicitly, and if so issues a gentler warning, off by default -

file.cpp:30:22: warning: MPOK01: Shared pointer without deleter
      using default-initialized allocator variable
    ManagedPtr<char> mp04(new (*da) char);
                     ^         ~~~
file.cpp:18:27: note: MPOK01: Initialization is here
    bslma::Allocator     *da = bslma::Default::allocator();
                          ^