|
Boost : |
From: Kevin S. Van Horn (kevin.vanhorn_at_[hidden])
Date: 2001-11-01 14:37:13
On Thu, 1 Nov 2001, David Abrahams wrote:
> [In reference to principle of making interfaces minimal but complete.]
> It may be a good principle "in principle", but it seems as though some of
> our most powerful concepts violate that principle. For example, iterators,
> numeric types, and containers. So, I don't put too much stock in it anymore.
The principle of making interfaces minimal but complete doesn't mean you
shouldn't have a rich set of convenience functions; it just means that you
should limit access to the innards of a class to those functions that really
need it (for either functionality or performance), and implement the rest of
the interface as non-member functions built on top of the minimal interface.
This limits the amount of code you have to review when you modify the class
implementation. This minimalist approach is just what you get when you take
encapsulation seriously. Of course, it does require more thought than the
naive approach (especially beloved by Java programmers) of "if it operates on
the class, it should be a method of the class".
With all that in mind, let's look at your examples:
1. Iterators. Yes, iterators have a lot of convenience functions that are not
strictly necessary, such as operator->(), post-increment, etc. But most of
these can, and usually are, implemented in terms of a much smaller set of
basic operations (operator*(), pre-increment, copy ctor). The only reason
many of these (post-increment, operator->()) are made member functions is that
C++ won't allow you to do it any other way. Many of the remaining operations
(comparison, addition, and subtraction for random-access iterators) are
often implemented as free functions on top of a minimal set of functions.
2. Numeric types. These follow the minimal-but-complete principle well.
Very few numeric operations know or care about the internal details of a
number's representation; instead, they are built on top of a small number of
basic arithmetic operations, plus a few performance enhancers such as ldexp()
and frexp().
3. Containers. The basic_stream<> class is an example that does not follow
the principle well, and it's an ungodly mess. I've yet to meet anybody who's
really happy with it. Most of the other containers in the standard library
come from STL, and one tends to operate on them using generic algorithms
instead of class-specific member functions. If you look at the remaining
container member functions that are not strictly necessary, they tend to fall
into one of these groups:
A. Functions that can be implemented much more efficiently if you have access
to the container internals. This falls under "necessary for performance", and
hence may be considered part of a minimal interface.
B. Functions that C++ requires to be member functions. For example, ctors
have to be member functions. This is a shame, because it would be nice if one
could implement new ctors on top of existing ctors without having to change
the class declaration or define a subclass. (The idea is that the new ctor
would call some previously-existing ctor after computing its arguments.)
As another example, operator=() has to be a member function, even though at
least one common, strongly exception safe implementation of operator=()
requires no access to the innards of the class:
foo & operator=(foo const & x)
{
foo tmp(x);
swap(tmp);
}
C. Functions that don't really need access to the class internals and could be
non-member functions, but are made member functions only because people are so
wedded to the object.function(args) syntax. rbegin() and rend() are
examples: they can be (and usually are) implemented using begin() and end(),
together with the reverse_iterator or reverse_bidirectional_iterator adaptor.
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk