I l@ve RuBoard Previous Section Next Section

Alternative: The Standard Library Approach

I happen to like the syntax and usability of the preceding functions, but there are still some nifty things they won't let you do. Consider another approach that follows the style of the standard library.

  1. Copying

    
    
    template<class RAIter> 
    
    
    fixed_vector( RAIter first, RAIter last )
    
    
    {
    
    
      copy( first,
    
    
            first+min(size,(size_t)last-first),
    
    
            begin() );
    
    
    }
    
    
    

    Now when copying, instead of writing:

    
    
    fixed_vector<char,6> v; 
    
    
    fixed_vector<int,4>  w(v);  // initialize using 4 values
    
    
    

we need to write:



fixed_vector<char,6> v; 


fixed_vector<int,4>  w(v.begin(), v.end());


                            // initialize using 4 values


Stop and think about this for a moment. What do you think of it? For construction, which style is better梩he style of our proposed solution or this standard library-like style?

In this case, our initial proposed solution is somewhat easier to use, whereas the standard library-like style is much more flexible (for example, it allows users to choose subranges and copy from other kinds of containers). You can take your pick or simply supply both flavors.

  1. Assignment

    Note that we can't templatize assignment to take an iterator range, because operator=() may take only one parameter. Instead, we can provide a named function:

    
    
    template<class Iter> 
    
    
    fixed_vector<T,size>&
    
    
    assign( Iter first, Iter last )
    
    
    {
    
    
      copy( first,
    
    
            first+min(size,(size_t)last-first),
    
    
            begin() );
    
    
      return *this;
    
    
    }
    
    
    

Now when assigning, instead of writing:



w = v;                      // assign using 4 values 


we need to write:



w.assign(v.begin(), v.end()); 


                            // assign using 4 value


Technically, assign() isn't even necessary, because we could still get the same flexibility without it, but that would be uglier and less efficient:



w = fixed_vector<int,4>(v.begin(), v.end()); 


    // initialize and assign using 4 values


Again, stop and consider these alternatives. For assignment, which style is better梩he style of our proposed solution or this standard library-like style?

This time, the flexibility argument doesn't hold water because the user can just as easily (and even more flexibly) write the copy himself. Instead of writing:



w.assign( v.begin(), v.end() ); 


the user just writes:



copy( v.begin(), v.begin()+4, w.begin() ); 


So there's little reason to write an iterator-range assign() in this case. And for assignment, it's probably best to use the technique from the proposed solution and let clients use copy() directly whenever subrange assignment is desired.

Why Write the Default Constructor?

Finally, why does the proposed solution also write an empty default constructor, which merely does the same thing as the compiler-generated default constructor?

The answer is short and sweet: This is necessary because as soon as you define a constructor of any kind, the compiler will not generate the default one for you, and, clearly, client code such as the above requires it.

A Lingering Problem

This Item's question also asked: Does this design or code have any flaws?

Perhaps. Later in this book we'll distinguish between various exception safety guarantees (see Items 8 to 11, and page 38). Like the compiler-generated copy assignment operator, our templated assignment operator provides the basic guarantee, which can be perfectly fine. Just for a moment, though, let's explore what happens if we do want it to provide the strong guarantee, to make it strongly exception-safe. Recall that the templated assignment operator was defined as:



template<typename O, size_t osize> 


fixed_vector<T,size>&


operator=( const fixed_vector<O,osize>& other )


{


  copy( other.begin(),


        other.begin()+min(size,osize),


        begin() );


  return *this;


}


If one of the T assignments fails during the copy() operation, the object will be in an inconsistent state. Some of the contents of our fixed_vector object will be what they were before the failed assignment, but other parts of the contents will already have been updated.

Alas, as it is currently designed, fixed_vector cannot be made strongly exception-safe for assignment. Why not? Because of the following:

  • Normally, the right (and easiest) way to solve this is to supply an atomic and nonthrowing Swap() function that swaps the guts of two fixed_vector objects, and then simply use the canonical (and, this time, templated) form of operator=(), which simply uses the create-a-temporary-and-swap idiom. See Item 13 for details.

  • There's no way to write an atomic nonthrowing Swap() that exchanges the internals of two fixed_vector objects. This is because the fixed_vector internals are stored as a simple array that can't be copied in one atomic step梬hich, after all, was the very problem with our proposed operator=() that got us onto this side track.

But don't despair just yet. There is, indeed, a good solution, but it involves changing fixed_vector's design梚f only ever so slightly梥o that it stores its contents as a dynamically allocated array instead of as a member array. True, this eliminates the efficiency advantage of fixed_vector, and it means we have to write a destructor梑ut such can be the price of strong exception safety.



// A strongly exception-safe version: 


//


template<typename T, size_t size>


class fixed_vector


{


public:


  typedef T*       iterator;


  typedef const T* const_iterator;





  fixed_vector() : v_( new T[size] ) { }





  ~fixed_vector() { delete[] v_; }





  template<typename O, size_t osize>


  fixed_vector( const fixed_vector<O,osize>& other )


    : v_( new T[size] )


    { try {copy(other.begin(),other.begin()+min(size,osize),


               begin());}


      catch(...) { delete[] v_; throw; }}


  fixed_vector( const fixed_vector<T,size>& other )


    : v_( new T[size] )


    { try {copy(other.begin(), other.end(), begin());}


      catch(...) { delete[] v_; throw; }}





  void Swap( fixed_vector<T,size>& other ) throw()


  {


    swap( v_, other.v_ );


  }





  template<typename O, size_t osize>


  fixed_vector<T,size>& operator=(


    const fixed_vector<O,osize>& other )


  {


    fixed_vector<T,size> temp( other ); // does all the work


    Swap( temp ); return *this;         // this can't throw


  }


  fixed_vector<T,size>& operator=(


    const fixed_vector<T,size>& other ) {


    fixed_vector<T,size> temp( other ); // does all the work


    Swap( temp ); return *this;         // this can't throw


  }





  iterator       begin()       { return v_; }


  iterator       end()         { return v_+size; }


  const_iterator begin() const { return v_; }


  const_iterator end()   const { return v_+size; }





private:


  T* v_;


};


Common Mistake

graphics/commonmistake_icon.gif

Never make exception safety an afterthought. Exception safety affects a class's design. It is never "just an implementation detail."


To summarize: This Item, I hope, has convinced you that member function templates are handy. I hope it's also helped to show why they're widely used in the standard library. If you're not familiar with them already, don't despair. Not all compilers support member templates today, but they're in the standard, therefore all compilers will soon.

Use member templates to good effect when creating your own classes and you'll likely not just have happy users, but more of them, as they flock to reuse the code that's best designed for reuse.

    I l@ve RuBoard Previous Section Next Section