Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

How to Ensure Proper Dynamic Library Boundary Crossing for Objects using Smart Pointers' Custom Deleters

4.83/5 (25 votes)
24 Feb 2023MIT4 min read 57K   560  
Solution using smart pointers' custom deleters
Ensuring that objects allocated in one dynamic library are deleted by the same library has always been a challenge. Solutions prior to the advent of C++11 often make the usage of the library cumbersome. This is a solution using smart pointers' custom deleters.

Introduction

Many prominent C++ experts promote the usage of smart pointers instead of raw pointers to a point where they claim that in modern C++, the visible usage of the keyword new should disappear. All dynamic allocations should be hidden by the Standard Library, either with containers like std::vector or with smart pointers.

The standard library's smart pointers can be customized in the way they handle the de-allocation of the memory they hold. This feature is the key to make elegant boundary crossing possible as suggested by this article.

Background

An object is said to cross a dynamic library boundary when it is instantiated within an assembly and consumed by another assembly. A common way this happens is when a factory-like structure instantiates objects and returns pointers to them in a dynamic link library.

For example, let's say another library or an executable that links with this library uses the factory to dynamically instantiate and retrieve a pointer to an object. The assembly that consumes the pointer can do anything with it, including a delete on the pointer to free the memory it points to. If the library that allocates the memory and the assembly that consumes the pointer use different versions of the dynamic memory allocation OS run-time library (called the CRT on Windows), there is going to be a problem in the memory allocation book-keeping. For a Microsoft specific example of the problem, see this.

Image 1

Typically, before the advent of C++11, library developers had to provide functions for de-allocation of objects that were allocated within their library's boundaries, in order to avoid this problem. This had the undesirable side effect that interfaces of such libraries were heavier and required a per-library specific know-how to correctly allocate and de-allocate objects of the library.

An ideal case would be an allocation/de-allocation scheme that the user doesn't need to know about. They just call the allocation mechanism of the library (e.g., a factory) and don't even bother about de-allocation.

Using the Code

The code related to this article is divided in two projects. The first project is ExecutableConsumer, which is a simple main file that uses a library's factory to instantiate objects from the library. The second project is LibraryFactory, which illustrates a problematic situation and the solution.

The problematic situation is a factory (called ProblematicFactory) that instantiates an object and returns a raw pointer to it. The solution is another factory (called SafeFactory) that instantiates an object and returns a std::unique_ptr to it, having its custom deleter properly set so that the de-allocation is done in the DLL.

If you run the program in debug mode in Visual Studio, with the macro USE_PROBLEMATIC_FACTORY_AND_CAUSE_HEAP_CORRUPTION defined, you will be able to see that the debugger detects a heap corruption.

Note that the projects provided in the solution are willingly linking with different version of the CRT in order to illustrate the heap corruption problem.

Since code is worth a thousand words, the following sections will be mainly code containing didactic comments from the attached file.

The Executable's Main File

Note that contexts are created in the main (by using curly braces) to encapsulate individual examples. Remember that at the exit of a context, all local variables are destroyed.

C++
#include <ProblematicFactory.h>
#include <SafeFactory.h>

// change this undef to a define to see the heap corruption assert in debug
#undef USE_PROBLEMATIC_FACTORY_AND_CAUSE_HEAP_CORRUPTION

int main()
{
#ifdef USE_PROBLEMATIC_FACTORY_AND_CAUSE_HEAP_CORRUPTION
  {
    // this allocation is done in the DLL
    auto wMyObject = ProblematicFactory::create();
    // this deallocation is done in the current assembly
    delete wMyObject;
    // when the DLL and this assembly are linked with the exact same CRT DLL,
    // the delete will work properly, otherwise, it will cause heap corruption.
  }
#endif

  {
    // always use auto when possible!
    auto wMyObject = SafeFactory::create();
    // When the program will hit the following curly brace, 
    // wMyObect will be automatically deleted
    // using the custom deleter provided in MyClass.h. 
    // No need to send it back to a de-allocation
    // function of the library!
  }

  {
    // std::unique_ptr can be automatically promoted to a std::shared_ptr
    // and the custom deleter follows, feels like magic!
    std::shared_ptr< MyClass > wMyObject = SafeFactory::create();
    // Same behavior as the example above, 
    // since the shared count will reach zero on the following
    // curly brace.
  }
 
  return 0;
}

The Library's Problematic Factory

This is a typical implementation of a factory that returns a raw pointer to objects it can create.

C++
#pragma once

#include "DllSwitch.h"
#include "MyClass.h"

class ProblematicFactory
{
public:
  static LIBRARYFACTORY_API MyClass * create();

private:
  ProblematicFactory();
};

The Library's Safe Factory

Syntactically, using this factory is essentially the same as using the problematic one (see in the main file), but it encapsulates the raw pointer in a std::unique_ptr.

C++
#pragma once

#include "DllSwitch.h"
#include "MyClass.h"

#include <memory>

class SafeFactory
{
public:  
  // Note that this function will not be not part of the DLL, so no std::unique_ptr
  // crosses a library boundary! It is going to be built in the client's translation
  // units, therefore it uses the std::unique_ptr of the client. Also note that there
  // is no need to explicitly provide a custom deleter, since a specialization of
  // std::default_delete exists for the class MyClass (see MyClass.h).
  inline static std::unique_ptr< MyClass > create()
  {
    return std::unique_ptr< MyClass >(doCreate());
  }

private:
  SafeFactory();

  static LIBRARYFACTORY_API MyClass * doCreate();
};

The Library's Object that Crosses Boundaries

Note that the default_delete class in this file is a specialization of a std class, so it needs to be in the std namespace.

C++
#pragma once
 
#include "DllSwitch.h"

#include <memory>

class LIBRARYFACTORY_API MyClass
{
};

// The following is a specialization of default_delete used by unique_ptr
// for the class MyClass. You need this for all types that the factory can create.
template<>
class LIBRARYFACTORY_API std::default_delete< MyClass >
{
public:
  void operator()(MyClass *iToDelete);
};

Points of Interest

Having specialization of std functions can seem strange at first, but this is a specialization for your classes. For some std templates, this is totally legal and std::default_delete is a perfect candidate for specialization. Consult this post addressing the question if you are interested.

Following a comment below, I have added MyClassPluginBuilder in the library, so that it is possible to instantiate MyClass from an executable that would have opened the library at runtime like a plugin.

History

  • v1.0 - 20th May, 2013: Initial release
  • v1.1 - 26th August, 2014: Added MyClassPluginBuilder and ExecutableConsumerPlugin

License

This article, along with any associated source code and files, is licensed under The MIT License