Arrays, shared_ptr< T >s, and deleters

In my last few posts about shared_ptr<T>s I’ve been using a struct called private_deleter. When you first attach a target object to a shared_ptr<T> you can also specify a deleter, which is a functor with an operator() that takes a single argument, a pointer to the target object, and deletes it.

  // Instances of some_type will be managed
  // with shared_ptr< some_type >s.
  struct
some_type
  { };

  // Deleter that works when you create
  // some_type with operator new.
  struct
some_type_deleter
  {   void
    operator ()( some_type * p)
      { delete p; }
  };

  // This shared_ptr has no deleter, so the
  // target is deleted with the function
  // boost::checked_delete< some_type >(..).
  shared_ptr< some_type >
sp_1(
  new some_type);

  // Boost provides a standard deleter struct
  // that calls boost::checked_delete<T>(..).
  shared_ptr< some_type >
sp_2(
  new some_type,
  boost::checked_deleter< some_type >( ));

  // This target object is bound to a new instance
  // of our deleter struct, defined above.
  shared_ptr< some_type >
sp_3(
  new some_type,
  some_type_deleter( ));

You use the deleter, of course, to control target object deletion. Which is symmetric since you also control target creation. Without the deleter you could only create target objects with operator new to match the operator delete assumed by shared_ptr.

Since the deleter becomes part of the intermediate object (aka the “control block”), you can also use it as a way to add variables and functions without intruding on the target class. For example, you can use it to store a list of notifier functors to be triggered when the target is deleted. Or you can use it as a chunk of memory in which you construct the target object. But in this post we’re just going to use the deleter to delete.

The Boost smart pointers provide a shared pointer just for arrays, called shared_array<T>. It is similar to shared_ptr<T> except it uses operator delete[] instead of operator delete to delete the target. But shared_array<T> is not part of TR1, and it is not necessary since shared_ptr<T> supports custom deleters.

The following shows how to use a deleter so shared_ptr<T> correctly deletes arrays.

# include <boost/detail/lightweight_test.hpp>
# include <boost/shared_ptr.hpp>
using boost::shared_ptr;

  // Deleter to correctly delete arrays
  // (remember it's OK to delete a const)
  template< typename T >
  struct
array_deleter
  {
      void
    operator ()( T const * p)
      { delete[] p; }
  };

  // Example target object
  struct
my_type
  { my_type( )  { balance += 1; }
    ~my_type( ) { balance -= 1; }
    static int balance;
  };
  int my_type::balance = 0;

  int
main( )
{
  // Create an array and bind it to an array_deleter
  // with shared_ptr. The my_type constructor is
  // called 7 times, followed by 7 calls to the
  // destructor.
  {    shared_ptr< my_type >
    sp_array(
      new my_type[ 7 ],
      array_deleter< my_type >( ));
  }
  BOOST_TEST( 0 == my_type::balance);

  return boost::report_errors( );
}

Boost even provides an array-deleter class (checked_array_deleter<T>) so you don’t have to define one yourself.

# include <boost/shared_ptr.hpp>
using boost::shared_ptr;
using boost::checked_array_deleter;

  shared_ptr< my_type >
sp_array_inst(
  new my_type[ 53 ],
  checked_array_deleter< my_type >( ));

Since shared_array<T> is no longer necessary to support array deletion, I suspect it will never become part of the standard library. operator [] is not enough justification for its existence. It will languish in the Boost library, a barely supported dead-end experiment, and will never be integrated with shared_ptr and weak_ptr.

As a final note, another way to work with arrays and shared_ptrs is to use an array wrapper like the TR1 array class. Since you can allocate these objects with operator new you can rely on the default shared_ptr deleter.

# include <boost/array.hpp>
using boost::array;

  shared_ptr< array< my_type, 4 > >
sp_array2(
  new array< my_type, 4 >);

But shared_ptr< array< my_type, 4 > > is a lot more awkward than simple shared_ptr< my_type >, and I doubt it will become a common idiom.

Specializing make_shared< T > and allocate_shared< T >

In my last post I talked about factory_type, a class that supplies a factory function make_new( ) as a static method.

(In retrospect, I suppose factory_type isn’t such a good name since it sounds like the instances are factories. Better names might be factory_made_object_type or gizmo_type.)

Anyway, the code looked like this:

# include <boost/shared_ptr.hpp>
using boost::shared_ptr;

  class
factory_type
{
    /* this_type alias */
    private:
    typedef
    factory_type
  this_type;

  // ======== Disable Copy ============

    /* disabled copy ctor */
    private:
  factory_type( this_type const &)
    ; /* no implementation */

    /* disabled copy assignment */
    private:
    void
  operator =( this_type const &)
    ; /* no implementation */

  // ======== Factory =================

    /* private default ctor */
    private:
  factory_type( )
    { }

    /* private dtor */
    private:
  ~factory_type( )
    { }

    /* the only place where the dtor is used */
    private:
    struct
  private_deleter
    {   void
      operator ()( this_type * p)
        { delete p; }
    };

    /* factory, only way to make these objects */
    public:
    static
    shared_ptr< this_type >
  make_new( )
    { return
        shared_ptr< this_type >(
          new this_type,
          private_deleter( ));
    }
};

Now there’s been some discussion (see here and here) about defining standard factory template functions called make_shared<T>(..) and allocate_shared<T,A>( A const &, ..) with signatures like this:

namespace std {

  template< typename T >
  shared_ptr< T >
make_shared( )
  ;

  template< typename T, typename ALLOC_T >
  shared_ptr< T >
allocate_shared( ALLOC_T const & allocator_inst)
  ;

  template< typename T, typename ... ARG_Ts >
  shared_ptr< T >
make_shared( ARG_Ts && ... args)
  ;

  template< typename T, typename ALLOC_T, typename ... ARG_Ts >
  shared_ptr< T >
allocate_shared( ALLOC_T const & alloc_inst, ARG_Ts && ... args)
  ;

} /* end namespace std */

It is easy to specialize make_shared< factory_type >( ) to use our class’s private factory.

namespace std {

  // specialize make_shared<T>( )
  template< >
  shared_ptr< factory_type >
make_shared< factory_type >( )
  { return factory_type::make_new( ); }

} /* end namespace std */

Let’s try some more complicated factories. Assume factory_type has some additional constructors and factories.

  class
factory_type
{
  ... as declared above ...

  // ======== Constructors =================

    /* private default ctor */
    private:
  factory_type( )
    ;

    /* private ctor */
    private:
  factory_type( float, void *, char = 'A')
    ;

    /* private ctor */
    private:
  factory_type( double, long, std::string const &)
    ;

  // ======== Factories =================

    public:
    static
    shared_ptr< this_type >
  make_new( )
    { return
        shared_ptr< this_type >(
          new this_type,
          private_deleter( ));
    }

    public:
	template< typename ... ARG_Ts >
    static
    shared_ptr< this_type >
  make_new( ARG_Ts && ... args)
    { return
        shared_ptr< this_type >(
          new this_type( std::forward< ARG_Ts >( args) ...),
          private_deleter( ));
    }

  // ======== Factories with allocators =======

    public:
	template< typename ALLOC_T >
    static
    shared_ptr< this_type >
  allocate_new( ALLOC_T const & alloc_inst)
    { return
        shared_ptr< this_type >(
          new this_type,
          private_deleter( ),
		  alloc_inst);
    }

    public:
	template< typename ALLOC_T, typename ... ARG_Ts >
    static
    shared_ptr< this_type >
  allocate_new( ALLOC_T const & alloc_inst, ARG_Ts && ... args)
    { return
        shared_ptr< this_type >(
          new this_type( std::forward< ARG_Ts >( args) ...),
          private_deleter( ),
		  alloc_inst);
    }
};

We’ll find it much harder to specialize make_shared<T>(..) and allocate_shared<T,A>( A const &, ..) to use these class-supplied factories that take parameters because you cannot partially specialize a templated function like you can a templated struct. Forgetting that, you might try:

namespace std {

  // Illegal partial specialization of template function.
  // Does not compile!!
  template< typename A_T >
  shared_ptr< factory_type >
allocate_shared< factory_type, A_T >( A_T const & alloc_i)
  { return factory_type::allocate_new( alloc_i); }

} /* end namespace std */

And the compiler will choke and complain. You could try to fully specialize the template function since partial specialization is illegal, but that means you have to know the type of the allocator beforehand. And allocators tend to come in many types. Another approach would be to define a separate overloaded (not specialized) template function called allocate_new, but typename T would have to be the first template parameter, and the first thing you want to do is specialize that as factory_type. So that doesn’t improve things in this case.

We could make this work if the first template parameter was used in the function’s argument list. If instead of allocate_shared< factory_type >( alloc_inst) the idiom was allocate_shared_2( factory_type::tag( ), alloc_inst) then we could define another template function around ALLOC_T and we would not have to specialize. But in this case that’s not a very attractive option.

Another probably better option is to define allocate_shared<T,A>(..) as a simple call to something like shared_ptr_maker<T>::allocate(..). Then we could specialize the template class shared_ptr_maker<T> instead of the function.

And finally, I’m not sure, but these examples may be an abuse of make_shared and allocate_shared. These were originally proposed as a way to hide use of the new operator since shared_ptr also hides delete. But looking at Peter Dimov’s code suggests the purpose of these functions may now be to implement an allocation strategy, where the intermediate object (aka the “control block”) and the target object are allocated as one chunk of memory instead of as two. If that’s so, it’s unlikely you’d provide specializations or overloads for these functions.

Class factories and shared_ptr<T>

One way to control object memory and initialization is to wrap object creation in a class factory. This is the point of the proposed make_shared<T>(..) and allocate_shared<T>(..) standard library functions.

But it is easy to roll your own simple factory function:

# include <boost/shared_ptr.hpp>
using boost::shared_ptr;

  class
factory_type
{
    /* this_type type alias */
    private:
    typedef
    factory_type
  this_type;

  // ======== Disable Copy ============

    /* disabled copy ctor */
    private:
  factory_type( this_type const &)
    ; /* no implementation */

    /* disabled copy assignment */
    private:
    void
  operator =( this_type const &)
    ; /* no implementation */

  // ======== Factory =================

    /* private default ctor */
    private:
  factory_type( )
    { }

    /* public (unfortunately) dtor */
    public:
  ~factory_type( )
    { }

    /* factory, only way to make these objects */
    public:
    static
    shared_ptr< this_type >
  make_new( )
    { return
        shared_ptr< this_type >(
          new this_type);
    }
};

  shared_ptr< factory_type >
factory_inst(
  factory_type::make_new( ));

And there you have factor_type, a tightly controlled class where all insts are created in the factory (factory_type::make_new( )) and all deletes are through shared_ptr<T>s. The private constructors means not only can we not “new” the class ourselves, we also cannot use it as a supertype. Nor can we declare a factory_type class member or stack variable.

factor_type would be improved, however, if we could ensure that only shared_ptr<T>s could access the destructor. It doesn’t work to declare the destructor private and friend class shared_ptr< this_type > because the destructor is actually called from a deleter function. However we can friend the deleter function:

  class
factory_type
{
  ... as above ...

    /* private dtor */
    private:
  ~factory_type( )
    { }

    /* the only function that calls the dtor */
    friend void boost::checked_delete( this_type *);

    /* factory, only way to make these objects */
    public:
    static
    shared_ptr< this_type >
  make_new( )
    { return
        shared_ptr< this_type >(
          new this_type);
    }
};

This works fine, but you get the following warning from msvc++9.0: the inline specifier cannot be used when a friend declaration refers to a specialization of a function template. Of course you can disable this warning. It’s not very important and likely to go away in a future version of msvc++ anyway, and it may even be corrected by an optimized non-debug compile.

The nicest solution, however, is to provide a private non-templated deleter. It doesn’t issue the above warning and it feels more portable. It’s only drawback is that it slightly increases the size of the intermediate ref-count object.

  class
factory_type
{
  ... as above ...

    /* private dtor */
    private:
  ~factory_type( )
    { }

    /* the only place where the dtor is used */
    private:
    struct
  private_deleter
    {   void 
      operator ()( this_type * p)
        { delete p; }
    };

    /* factory, only way to make these objects */
    public:
    static
    shared_ptr< this_type >
  make_new( )
    { return
        shared_ptr< this_type >(
          new this_type,
          private_deleter( ));
    }
};

Next Page →