Udostępnij za pośrednictwem


Fun with templates, part one - abstracting operations:

[edited 7-23-08: updated title, tags]

I'm planning a talk on template use, and thought my blog might be a good way to gether my thoughts, and also to share some of the concepts that I've been working with. Over the next few days (or maybe weeks) I'll try to post a number of articles dealing with a variety of ways to get more mileage out of templates & related techniques. Tonight I want to show how to make even the  simplest templates a bit more flexible.

 Consider the following: you want to write a storage class that operates on a class T, but over the years, you've acquired a few different smart pointer libraries, and you need to replace the value in the pointer. You're smart pointer libraries all use different semantics though:

1) sp::replace(spObject, pValue)

2) pObject.Reset(pValue)

3) pObject = pValue

Now, without making a value judgement on which I prefer, how do you write a template that can work with all three types? One solution is to create a library of overloaded functions that we can use in our template to perform operations on the types that might be smart pointers. To handle the assign case above, we can use this:

namespace sp_ops

{

      template <class T>

      void assign( spa<T> & pointer, const T & value)

      {

            pointer.reset(value);

      }

      template <class T>

      void assign(spb<T> & pointer, const T & value)

      {

            spb<T>::reset(pointer, value);

      }

      template <class dest_t, class T>

      void assign( dest_t & target, const T & value)

      {

            target = value;

      }

};

The overloads for assign allow us to use a consistent semantic for assign in our template implementation regardless of which smart pointer is used as a template argument. This is a bit like a policy, but unlike a policy, we pre-configure the possible policies, and don't require the usr of the template to supply the correct policy for their pointer. Also, note taht by using two template parameters on the last variation, we create a catch-all. Anything that supports assignment, and isn't one of the two template types falls into the last bucket. Here's how this actually plays out in the template that we wanted to write in the first place:

template <class storage_t, class value_t>

class canister

{

public:

      void set( const value_t & value )

      {

            sp_ops::assign(contents, value);

      }

private:

      storage_t contents;

};

Could we have just written overloads for set? Not really, because the type that we wanted to change behavior on is a class template parameter, not a function parameter. Finally, it's worth noting that container is a very incomplete, rough class. It's just enough code to show the usage. As an interesting excercise, it's also worth noting that you can use this to handle diverse parameters as well. For example:

template <class storage_t, class value_t>

class canister

{

public:

       template <class T>

      assign_to( T & target )

      {

            sp_ops::assign(target, contents);

      }

private:

storage_t contents;

};

With this last variation, we can now assign out to anything that supports assignment semantics. To really support the last scenario, we may need to define versions of assign that define some transfer semantics for our intrusively ref counted objects, and of course there's not much we can do for transferring objects managed by shared pointers from one library to another. Assuming that the smart pointers are assignable to pointers of the same type though, and not implicitly convertable to bare pointers (this is almost always a bad idea, quite dangerous) this will cover the valid caess. If one of your smart pointers can implicitly convert to a bare pointer, you may want to create a prototype for the assign from that smart pointer to the other smart pointer types, but not give it a body. This prevents accidentally creating two owners for the same object. Any attempt to do so will result in use of an undefined function. A compile time assert in the function would also provide a meaningful check (I won't cover compile time asserts here. They are well documented elsewhere).

Well, that's enough fun for one night. I'll try to post again soon. If you have suggestions for topics related to templates, feel free to send suggestions via my feedback link. Note that for the time being, I'm going to try to avoid some of the heavier topics around template metaprogramming (I have some ideas queued up, but those need to wait until I get through some more basics!)

Thanks,

Ben

Comments