Re: Item 42 of Effective C++
From: Ulrich Eckhardt (doomster_at_knuut.de)
Date: 12/28/04
- Next message: Francis Glassborow: "Re: How to get better at C++?"
- Previous message: Jonathan Mcdougall: "Re: A little help please"
- In reply to: Phantom: "Item 42 of Effective C++"
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
Date: Tue, 28 Dec 2004 18:13:18 +0100
Phantom wrote:
> I am a bit confused after reading Item 42 of Effective C++. It talks
> about the usage of a class that stores void* and that templates that
> use a private inheritance of this class do not suffer from code bloats
> ?
>
> Is it true to assume that code bloats occur only if one of the member
> variable in the class are of the template type ? Suppose I have a
> template class defined as follows :
>
> template <class T>
> class blah
> {
> // methods omitted for brevity
> private:
> T* data_;
> }
>
> The above class when instantiated multiple times suffers a code bloat
> as opposed to a class that uses the idea of using private inheritance
> and storing the memers as void* ?
There is an ambiguous wording in your question: instantiating a class
several times does not lead to code bloat, and that is probably also not
what you mean. What you mean is instantiating a template (not necessarily
a class template!) that leads to code bloat.
Also, code-bloat is not caused by having this or that kind of pointer in a
class, of avoided by deriving privately or publicly from a class with a
void pointer. I'll try to explain a bit what is going on here.
Now, to fully understand what causes code bloat and what not, you have to
leave the fields of standard C++ a bit and look at real-world
implementations and possibly at the assembler output they generate (I
believe at least knowing a bit of assembler helps a lot for understanding
C and C++).
Take a look at this piece of code:
// generic node of a linked list
template<typename T>
struct node
{
node* previous;
node* next;
T value;
};
// return a pointer to the next element of a generic node
template<typename T>
node<T>* get_next( node<T>* current)
{
node<T>* next = current->next;
return next;
}
Everything get_next() does is load the pointer 'current' into a register,
adds the offset between 'current' and 'next' (this offset depends on the
binary layout of stuct node<T> in memory, but it is a fixed value) then
load the pointer from that address and return it.
Now, in a typical implementation, the offset between the struct and its
member 'next' is the same[2], regardless of T! In other words, the
function always generates the same assembler instructions. However, for
the compiler it might be difficult or impossible to determine[1] that a
whole family of functions generate the same assembler output, which is why
it generates these instructions again and again.
Now, the example in effective C++ (I don't know this book, so I can only
guess) probably would have done something like this to above code, in
order to avoid this code bloat:
// non-template base for a list node
struct node_base
{
node* previous;
node* next;
};
// return a pointer to the next element of a node_base
node_base* get_next( node_base* current)
{
node_base* tmp = current->next;
return tmp;
}
// node of a generic linked list
template<typename T>
struct node: public node_base
{
T value;
};
// return a pointer to the next element of a generic list
template<typename T>
node<T>* get_next( node<T>* current)
{
node_base* current_base = static_cast<node_base*>(current);
node_base* next_base = get_next_base(current_base);
node<T>* next = static_cast<node<T>*>(next_base);
return next;
}
What you see is that the class and the function have been split into two
parts, one part that depends on T and a part that doesn't. The part that
depends on T uses the part that doesn't in the cases where the particular
T doesn't make any difference.
Looking at the (now even more complicated) version of get_next(), you might
wonder what the advantage is. The point is, that assembler has no
types[4], so the code inside get_next() can be reduced by the two lines
that only convert between 'node<T>*' and 'node_base*'[3], which are not
reflected in the assembler output. The only thing that remains is a call
to get_next_base() and returning the result thereof. I hope you can now
imagine that it is rather easy for the compiler to figure out that calling
a function with the same arguments and returning its result can also be
eliminated.
Conclusion: get_next() is a thin wrapper around get_next_base(). This
wrapper gets completely eliminated by the compiler/optimizer.
get_next_base() on the other hand is not a template, so it is not subject
to code bloat.
Uli
[1] I first wanted to quote a function that walks over a tree to the next
element because this example is rather trivial to figure out. I decided
against it, because it is too complex for understanding. However, it still
does not use the value of its node, only the links (pointers) to parent
and children.
[2] It would have been different if the order in the struct was so that the
pointers came after the 'value' field!
[3] I wrote static_cast there and that it is not reflected in the assembly,
but that is a bit sloppy. In the case of multiple baseclasses, a
conversion to a baseclass or from a baseclass back to the derived class
means modifying the pointer by a fixed offset.
[4] Well, almost, it usually distinguishes between integral type (including
pointers) and floating point types, and of course between different sizes.
For almost all cases however, a pointer is a pointer, no matter what C++
object it is pointing to.
-- FAQ: http://ma.rtij.nl/acllc-c++.FAQ.html
- Next message: Francis Glassborow: "Re: How to get better at C++?"
- Previous message: Jonathan Mcdougall: "Re: A little help please"
- In reply to: Phantom: "Item 42 of Effective C++"
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
Relevant Pages
|