Tuples and structs

The TR1 library for C++0X includes the template type tuple<..>. It’s available now from Boost.

# include <boost/tuple/tuple.hpp>
# include <boost/tuple/tuple_comparison.hpp>
# include <boost/tuple/tuple_io.hpp>

using boost::tuples::tuple;

tuple< double, char > tup_inst1;
tuple< double, char > tup_inst2( 3.22);
tuple< double, char > tup_inst3( 5.42, 'a');

Abstraction

A tuple is a lot like an old C-style POD struct. Both are heterogeneous aggregators, holding a collection of objects of various types. Neither are open ended, as the types and counts of the enclosed values are fixed at compile time.

Of course a modern C++ struct can also have methods, supertypes, static members, etc. A tuple doesn’t do any of that. It is bare and public, like struct circa 1980, before templates, when compilers were not expected to generate code.

So how are they different? Why bother with tuple when you can just use struct?

You don’t have to declare a tuple<..> type before you use it, like you do with struct. You just use tuples when you need one. They are declared on-the-fly, at the point of usage.

tuples and structs both provide a default constructor, a destructor, a copy constructor, and a copy assignment operator. tuple are much more flexible during copy however, converting compatible inside types when possible. A struct enforces its type, while a tuple is just bundling values.

A tuple also provides constructors that can explicitly set member variables. With structs you either have to write the (obvious) constructor yourself, or use compiler initialization lists when that’s possible.

tuple types also have pre-defined comparison operators and IO functions (operator <<).

Unlike tuples, struct types are (usually) named. tuples are thin wrappers around their innards. They are not modest. structs attempt more abstraction, both by being named and by using names to access members.

structs protect their identity, sometimes to a fault. You cannot declare an identical named struct twice in the same compilation unit. Identical anonymous structs are different types. This can be inconvenient.

  // You cannot use a compiler initialization
  // list, at least with MSVC9 and GCC3.4.
  // I suspect this will change in C++0X. After all,
  // tuples feel even more POD than structs.
  tuple< double, char >
tuple_inst1
  ( 3.22, 'a'); // = { 3.22, 'a' }

  // Tuples know how to share.
  tuple< double, char >
tuple_inst2
  = tuple_inst1;

  // Using an unnamed struct. The member vars
  // have to have names though.
  struct { double a; char b; }
struct_inst1
  = { 3.22, 'a' };

  // This fails -- type mismatch.
  // Even though the structs are identical.
  // Structs really protect their identity.
  struct { double a; char b; }
struct_inst2
  = struct_inst1;

A struct is an abstractor, a hider of information, while a tuple is just an aggregator. For tuples, structure determines type. For structs, the name determines type and provides abstraction. But sometimes you don’t want abstraction, or even a name.

Metaprogramming

You access tuple members through integer indexes (i.e. tuple_inst.get<2>()). They’re like heterogeneous arrays. Contrast that with struct member access, which is by name (i.e. struct_inst.cost_increase).

Index access is mostly an artifact of C++ templates, which can manipulate integers and types, but aren’t so good with names (although you can use enums to give names to index values). Lisp macros have finer-grained control, manipulating symbols (names) as well as constants and lists.

But index access does not feel unnatural because order is imposed anyway, in parameter lists and constructors. tuple<char,double> is not the same type as tuple<double,char>. Even so, there are occasions when it’s clearer to define an enum so you can say tuple_inst.get() instead of tuple_inst.get<3>(). But if you find yourself doing this, consider using a struct instead of a tuple.

The tuple type in Boost is built using a type list, a technique pioneered by Andrei Alexandrescu, creator of Loki and author of Modern C++ Design. A type list is a recursive set of templated types that can be aggregated into a larger type. I use a similar technique in the post Using templates to define an array class with constructors.

In the C++0X future tuple might be defined with variadic template parameters instead of a type list. The type list is just a technique, and it is not exposed in the interface. It is clever but not essential. A variadic implementation would be more straightforward.

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.

← Previous PageNext Page →