15.3 Policy Traits

Ru-Brd

So far, our examples of traits templates have been used to determine properties of template parameters: what sort of type they represent, to which type they should promote in mixed-type operations, and so forth. Such traits are called property traits .

In contrast, some traits define how some types should be treated. We call them policy traits . This is reminiscent of the previously discussed concept of policy classes (and we already pointed out that the distinction between traits and policies is not entirely clear), but policy traits tend to be more unique properties associated with a template parameter (whereas policy classes are usually independent of other template parameters).

Although property traits can often be implemented as type functions, policy traits usually encapsulate the policy in member functions. As a first illustration, let's look at a type function that defines a policy for passing read-only parameters.

15.3.1 Read-only Parameter Types

In C and C++, function call arguments are passed "by value" by default. This means that the values of the arguments computed by the caller are copied to locations controlled by the callee. Most programmers know that this can be costly for large structures and that for such structures it is appropriate to pass the arguments "by reference-to- const " (or "by pointer-to- const " in C). For smaller structures, the picture is not always clear, and the best mechanism from a performance point of view depends on the exact architecture for which the code is being written. This is not so critical in most cases, but sometimes even the small structures must be handled with care.

With templates, of course, things get a little more delicate: We don't know a priori how large the type substituted for the template parameter will be. Furthermore, the decision doesn't depend just on size: A small structure may come with an expensive copy constructor that would still justify passing read-only parameters "by reference-to- const ."

As hinted at earlier, this problem is conveniently handled using a policy traits template that is a type function: The function maps an intended argument type T onto the optimal parameter type T or T const& . As a first approximation , the primary template can use "by value" passing for types no larger than two pointers and "by reference-to- const " for everything else:

 template<typename T>  class RParam {    public:      typedef typename IfThenElse<sizeof(T)<=2*sizeof(void*),                                  T,                                  T const&>::ResultT Type;  }; 

On the other hand, container types for which sizeof returns a small value may involve expensive copy constructors. So we may need many specializations and partial specializations, such as the following:

 template<typename T>  class RParam<Array<T> > {    public:      typedef Array<T> const& Type;  }; 

Because such types are common in C++, it may be safer to mark nonclass types "by value" in the primary template and then selectively add the class types when performance considerations dictate it (the primary template uses IsClassT<> from page 266 to identify class types):

  // traits/rparam.hpp  #ifndef RPARAM_HPP  #define RPARAM_HPP  #include "ifthenelse.hpp"  #include "isclasst.hpp"  template<typename T>  class RParam {    public:      typedef typename IfThenElse<IsClassT<T>::No,                                  T,                                  T const&>::ResultT Type;  };  #endif  // RPARAM_HPP  

Either way, the policy can now be centralized in the traits template definition, and clients can exploit it to good effect. For example, let's suppose we have two classes, with one class specifying that calling by value is better for read-only arguments:

  // traits/rparamcls.hpp  #include <iostream>  #include "rparam.hpp"  class MyClass1 {    public:      MyClass1 () {      }      MyClass1 (MyClass1 const&) {          std::cout << "MyClass1 copy constructor called\n";      }  };  class MyClass2 {    public:      MyClass2 () {      }      MyClass2 (MyClass2 const&) {          std::cout << "MyClass2 copy constructor called\n";      }  };  // pass  MyClass2  objects with  RParam<>  by value  template<>  class RParam<MyClass2> {    public:      typedef MyClass2 Type;  }; 

Now, you can declare functions that use RParam<> for read-only arguments and call these functions:

  // traits/rparam1.cpp  #include "rparam.hpp"  #include "rparamcls.hpp"  // function that allows parameter passing by value or by reference  template <typename T1, typename T2>  void foo (typename RParam<T1>::Type p1,            typename RParam<T2>::Type p2)  {   }  int main()  {      MyClass1 mc1;      MyClass2 mc2;      foo<MyClass1,MyClass2>(mc1,mc2);  } 

There are unfortunately some significant downsides to using RParam . First, the function declaration is significantly more mess. Second, and perhaps more objectionable , is the fact that a function like foo() cannot be called with argument deduction because the template parameter appears only in the qualifiers of the function parameters. Call sites must therefore specify explicit template arguments.

An unwieldy workaround for this option is the use of an inline wrapper function template, but it assumes the inline function will be elided by the compiler. For example:

  // traits/rparam2.cpp  #include "rparam.hpp"  #include "rparamcls.hpp"  // function that allows parameter passing by value or by reference  template <typename T1, typename T2>  void foo_core (typename RParam<T1>::Type p1,                 typename RParam<T2>::Type p2)  {   }  // wrapper to avoid explicit template parameter passing  template <typename T1, typename T2>  inline  void foo (T1 const & p1, T2 const & p2)  {      foo_core<T1,T2>(p1,p2);  }  int main()  {      MyClass1 mc1;      MyClass2 mc2;      foo(mc1,mc2);  // same as  foo_core<MyClass1,MyClass2>(mc1,mc2)  } 

15.3.2 Copying, Swapping, and Moving

To continue the theme of performance, we can introduce a policy traits template to select the best operation to copy, swap, or move elements of a certain type.

Presumably, copying is covered by the copy constructor and the copy-assignment operator. This is definitely true for a single element, but it is not impossible that copying a large number of items of a given type can be done significantly more efficiently than by repeatedly invoking the constructor or assignment operations of that type.

Similarly, certain types can be swapped or moved much more efficiently than a generic sequence of the classic form:

 T tmp(a);  a = b;  b = tmp; 

Container types typically fall in this category. In fact, it occasionally happens that copying is not allowed, whereas swapping or moving is fine. In the chapter on utilities, we develop a so-called smart pointer with this property (see Chapter 20).

Hence, it can be useful to centralize decisions in this area in a convenient traits template. For the generic definition, we will distinguish class types from nonclass types because we need not worry about user -defined copy constructors and copy assignments for the latter. This time we use inheritance to select between two traits implementations :

  // traits/csmtraits.hpp  template <typename T>  class CSMtraits : public BitOrClassCSM<T, IsClassT<T>::No > {  }; 

The implementation is thus completely delegated to specializations of BitOrClassCSM<> (" CSM " stands for "copy, swap, move"). The second template parameter indicates whether bitwise copying can be used safely to implement the various operations. The generic definition conservatively assumes that class types can not be bitwised copied safely, but if a certain class type is known to be a plain old data type (or POD ), the CSMtraits class is easily specialized for better performance:

 template<>  class CSMtraits<  MyPODType  >   : public BitOrClassCSM<  MyPODType  , true> {  }; 

The BitOrClassCSM template consists, by default, of two partial specializations. The primary template and the safe partial specialization that doesn't copy bitwise is as follows :

  // traits/csm1.hpp  #include <new>  #include <cassert>  #include <stddef.h>  #include "rparam.hpp"  // primary template  template<typename T, bool Bitwise>  class BitOrClassCSM;  // partial specialization for safe copying of objects  template<typename T>  class BitOrClassCSM<T, false> {    public:      static void copy (typename RParam<T>::ResultT src, T* dst) {  // copy one item onto another one  *dst = src;      }      static void copy_n (T const* src, T* dst, size_t n) {  // copy  n  items onto  n  other ones  for (size_tk=0;k<n; ++k) {              dst[k] = src[k];          }      }      static void copy_init (typename RParam<T>::ResultT src,                             void* dst) {  // copy an item onto uninitialized storage  ::new(dst) T(src);      }      static void copy_init_n (T const* src, void* dst, size_t n) {  // copy  n  items onto uninitialized storage  for (size_tk=0;k<n; ++k) {              ::new((void*)((char*)dst+k)) T(src[k]);          }      }      static void swap (T* a, T* b) {  // swap two items  T tmp(a);          *a = *b;          *b = tmp;      }      static void swap_n (T* a, T* b, size_t n) {  // swap  n  items  for (size_tk=0;k<n; ++k) {              T tmp(a[k]);              a[k] = b[k];              b[k] = tmp;          }      }      static void move (T* src, T* dst) {  // move one item onto another  assert(src != dst);          *dst = *src;          src->~T();      }      static void move_n (T* src, T* dst, size_t n) {  // move  n  items onto  n  other ones  assert(src != dst);          for (size_tk=0;k<n; ++k) {              dst[k] = src[k];              src[k].~T();          }      }      static void move_init (T* src, void* dst) {  // move an item onto uninitialized storage  assert(src != dst);          ::new(dst) T(*src);          src->~T();      }      static void move_init_n (T const* src, void* dst, size_t n) {  // move  n  items onto uninitialized storage  assert(src != dst);          for (size_tk=0;k<n; ++k) {              ::new((void*)((char*)dst+k)) T(src[k]);              src[k].~T();          }      }  }; 

The term move here means that a value is transferred from one place to another, and hence the original value no longer exists (or, more precisely, the original location may have been destroyed ). The copy operation, on the other hand, guarantees that both the source and destination locations have valid and identical values. This should not be confused with the distinction between memcpy () and memmove () , which is made in the standard C library: In that case, move implies that the source and destination areas may overlap, whereas for copy they do not. In our implementation of the CSM traits, we always assume that the sources and destinations do not overlap. In an industrial-strength library, a shift operation should probably be added to express the policy for shifting objects within a contiguous area of memory (the operation enabled by memmove() ). We omit it for the sake of simplicity.

The member functions of our policy traits template are all static. This is almost always the case, because the member functions are meant to apply to objects of the parameter type rather than objects of the traits class type.

The other partial specialization implements the traits for bitwise types that can be copied:

  // traits/csm2.hpp  #include <cstring>  #include <cassert>  #include <stddef.h>  #include "csm1.hpp"  // partial specialization for fast bitwise copying of objects  template <typename T>  class BitOrClassCSM<T,true> : public BitOrClassCSM<T,false> {    public:      static void copy_n (T const* src, T* dst, size_t n) {  // copy  n  items onto  n  other ones  std::memcpy((void*)dst, (void*)src, n);      }      static void copy_init_n (T const* src, void* dst, size_t n) {  // copy  n  items onto uninitialized storage  std::memcpy(dst, (void*)src, n);      }      static void move_n (T* src, T* dst, size_t n) {  // move  n  items onto  n  other ones  assert(src != dst);          std::memcpy((void*)dst, (void*)src, n);      }      static void move_init_n (T const* src, void* dst, size_t n) {  // move  n  items onto uninitialized storage  assert(src != dst);          std::memcpy(dst, (void*)src, n);      }  }; 

We used another level of inheritance to simplify the implementation of the traits for bitwise types that can be copied. This is certainly not the only possible implementation. In fact, for particular platforms it may be desirable to introduce some inline assembly (for example, to take advantage of hardware swap operations).

Ru-Brd


C++ Templates
C++ Templates: The Complete Guide
ISBN: 0201734842
EAN: 2147483647
Year: 2002
Pages: 185

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net