Boost logo

Glas :

Re: [glas] glas Digest, Vol 25, Issue 1

From: Bert Rodiers (bert.rodiers_at_[hidden])
Date: 2007-04-23 12:36:38


Hello Robert, Karl,

On 05/04/07, Robert P. Goddard <Robert_Goddard_at_[hidden]> wrote:
> Karl,
>
> I'm generally supportive of your idea of using free functions and traits
> in place of member functions and member typedefs. However, I caution
> against doing away with the latter. The Standard Library provides a good
> example (as it often does):

<snip>

It is interesting that you refer to the STL for this. I was just
reading the document "Notes on Programming", by Alexander Stepanov (
http://www.stepanovpapers.com/notes.pdf ). Stepanov is the designer
and original implementer of the Standard Template Library, and on page
21 he writes

<quote>
While we could make a member function to return length, it is better
to make it a
global friend function. If we do that, we will be able eventually to
define the same
function to work on built-in arrays and achieve greater uniformity of
design. I made
size into a member function in STL in an attempt to please the
standard committee. I
knew that begin, end and size should be global functions but was not
willing to risk
another fight with the committee. In general, there were many
compromises of which I
am ashamed. It would have been harder to succeed without making them,
but I still get a
metallic taste in my mouth when I encounter all the things that I did
wrong while
knowing full how to do them right. Success, after all, is much
overrated. I will be
pointing to the incorrect designs in STL here and there: some were
done because of
political considerations, but many were mistakes caused by my
inability to discern
general principles.)
</quote>

Given the feelings of the original STL-designer on this matter it
seems better to me not to take it as an example (except when you want
to achieve the same look & feel as STL).

David Abrahams and Aleksey Gurtovoy make a similar point in their book
"C++ Template Metaprogramming: Concepts, Tools, and Techniques from
Boost and Beyond" about the usage of traits in the STL:

<quote>
The traits templates in the standard library all follow the "multiple
return values" model. We refer to this kind of traits template as a
"blob," because it's as though a handful of separate and loosely
related metafunctions were mashed together into a single unit. We will
avoid this idiom at all costs, because it creates major problems.

First of all, there's an efficiency issue: The first time we reach
inside the iterator_traits for its ::value_type, the template will be
instantiated. That means a lot of things to the compiler, but to us
the important thing is that at that point the compiler has to work out
the meaning of every declaration in the template body that happens to
depend on a template parameter. In the case of iterator_traits, that
means computing not only the value_type, but the four other associated
types as welleven if we're not going to use them. The cost of these
extra type computations can really add up as a program grows, slowing
down the compilation cycle. Remember that we said type computations
would get much more interesting? "More interesting" also means more
work for your compiler, and more time for you to drum your fingers on
the desk waiting to see your program work.

Second, and more importantly, "the blob" interferes with our ability
to write metafunctions that take other metafunctions as arguments. To
wrap your mind around that, consider a trivial runtime function that
accepts two function arguments:

   template <class X, class UnaryOp1, class UnaryOp2>
   X apply_fg(X x, UnaryOp1 f, UnaryOp2 g)
   {
       return f(g(x));
   }

That's not the only way we could design apply_fg, though. Suppose we
collapsed f and g into a single argument called blob, as follows:

   template <class X, class Blob>
   X apply_fg(X x, Blob blob)
   {
       return blob.f(blob.g(x));
   }

The protocol used to call f and g here is analogous to the way you
access a "traits blob": to get a result of the "function," you reach
in and access one of its members. The problem is that there's no
single way to get at the result of invoking one of these blobs. Every
function like apply_fg will use its own set of member function names,
and in order to pass f or g on to another such function we might need
to repackage it in a wrapper with new names.

"The blob" is an anti-pattern (an idiom to be avoided), because it
decreases a program's overall interoperability, or the ability of its
components to work smoothly together. The original choice to write
apply_fg so that it accepts function arguments is a good one, because
it increases interoperability.

When the callable arguments to apply_fg use a single protocol, we can
easily exchange them:

   #include <functional>
   float log2(float);

   int a = apply_fg(5.Of, std::negate<float>(), log2);
   int b = apply_fg(3.14f, log2, std::negate<float>());

The property that allows different argument types to be used
interchangeably is called polymorphism; literally, "the ability to
take multiple forms."

To achieve polymorphism among metafunctions, we'll need a single way
to invoke them. The convention used by the Boost libraries is as
follows:

   metafunction-name<type-arguments...>::type

>From now on, when we use the term metafunction, we'll be referring to
templates that can be "invoked" with this syntax.
</quote>

Regards,
Bert