C++ gurus have been exhorting us to pass
parameters into functions via const reference since time immemorial.
For a large-sized type, this makes perfect sense: You're pushing
less stuff onto the stack, saving copying time (as well as constructor
calls).
But if you think about it long enough, you'll realize you shouldn't
do this all of the time. Many types of objects take up as much
space as (or less space than) any reference. char, many int types, enumerations, float,
small-enough classes, and most importantly, pointers. When you pass
objects of these types into a function by const reference, you pay the
penalty of extra indirections to get to the object values. So, smaller
objects are ideally passed in by value.
In a traditional function this doesn't matter so much. You know
which type you are using, and you can tailor parameters to suit the type
being used. But in a template you lose some flexibility: there's only
one main template declaration, and you can only declare it one way.
This problem appears all over the C++ Standard Library (descended from
the old Standard Template Library), which uses const references with reckess abandon
(incidentally screwing up the C++ function binders).
Of course, you could specialize the template for all
of the smaller types, but there are a LOT of them, and your code becomes
much harder to maintain that way.
But don't lose hope. There's a way to get your compiler to automatically
determine whether a type should be passed in by value, or by const reference. The following
C++ template class illustrates how to do it:
template <class T>
class type_traits
{
template<bool TOO_BIG>
struct decide
{
typedef T const ¶meter;
};
template<>
struct decide<false>
{
typedef T parameter;
};
static const bool TOO_BIG=sizeof(T)>sizeof(void *);
public:
typedef typename decide<TOO_BIG>::parameter parameter;
};
The template uses the compile-time constant function sizeof
to pick one specialization of the decide template to tell us what
our parameter type should be. Since most C++ compilers implement references
as pointers with an attitude, sizeof(void *) is probably
the best choice for a cutoff size, but you might also want to use sizeof(T*).
So, if we have
template<class T> void foo(typename type_traits<T>::parameter
arg);
then the specialization foo<int> will give us foo(int),
and foo<double>will give us foo (double const &)or
foo (double), depending on the compiler's implementation of
double.
This last illustrates an added benefit of using this technique: When your code is ported to
another
platform, the parameter types will be automatically adjusted based upon their sizes.
In a class template, of course, you'll want a typedef to make things
easier on the eyes:
template<class T, size_t N>
class tuple
{
T t [N];
public:
typedef typename type_traits<T>::parameter parameter;
typedef T *iterator;
typedef T *const_iterator;
tuple(parameter it=T());
tuple (tuple const &i);
tuple fill (parameter ft);
T &operator[](size_t i) { return t[i]; }
parameter operator[](size_t i) const { return t[i]; }
};
The technique breaks down for small classes that have complicated constructors.
template <class T>
class deep_ptr
{
T *p;
public:
typedef T value_type;
typedef typename type_traits<T>::parameter value_parameter;
typedef T &value_reference;
deep_ptr(T *ip=0) { p=new T(*ip); }
deep_ptr (deep_ptr<T> const &i) { p=new T(*i.p); }
~deep_ptr() { delete p; }
value_parameter operator *() const { return *p; }
value_reference operator *() { return *p; }
T const * const operator->() const { return p; }
T * const operator->() { return p; }
};
Notice that the technique is perfectly all right for the template argument
T, but if you made a tuple<deep_ptr<T>, N> things
would be slower than they needed to be.
You may have a couple of problems compiling the above code if your C++
compiler isn't sophisticated enough:
A lot of compilers still don't allow for member templates, or have problems
with explicit specialization of member templates. Some of those,
however, allow for partial specialization of templates, and you can move
the decide template outside of the class:
template<class T, bool TOO_BIG>
struct decide
{
typedef T const ¶meter;
};
template<class T>
struct decide<T, false>
{
typedef T parameter;
};
-
Some compilers (notably Visual C++) have a problem with defining static
const members in a class and using them as constant expressions at compile
time. For these compilers, you will have to define TOO_BIG
in an enum (the venerable "enum hack"):
enum too_big_ { TOO_BIG=sizeof(T)>sizeof(void *) };
and you may need to static_cast TOO_BIG to get it to go into
the template:
typedef typename decide<static_cast<bool>(TOO_BIG)>::parameter
parameter;