|
Boost : |
From: Kevin S. Van Horn (kevin.vanhorn_at_[hidden])
Date: 2001-11-01 15:36:20
On Wed, 31 Oct 2001 williamkempf_at_[hidden] wrote:
> > [My description of "minimal but complete" class interfaces.]
> There's a reason to prefer (3) in a general sense. I forget the OO
> principles name, but in essence the public interface should
> be "minimal but complete". This principle can be taken too far,
> however.
Would you care to elaborate? What do you consider taking it too far, and what
criteria do you propose for when one should violate the principle?
> > [I discussed the problem with (1): Once a member function, always a
> > member function, even if in a later implementation the function doesn't
> need access to the class internals.]
>
> [...] I see nothing wrong with
> the function remaining a member function,
It's called *encapsulation*, something that OO enthusiasts claim to be in
favor of. Let me start from the beginning again, since I don't seem to be
communicating my point well. If you disagree with my analysis, please make
clear the specific point or points you disagree with and why.
1. You make your code easier to maintain and more reliable by decoupling
different pieces of code as much as possible. That is, you want the
correctness of code piece A to depend as little as possible on the details of
code piece B, and vice versa. This allows you to localize modifications to
the code. As someone who's spent many a tedious hour grepping through source
code to make one small change to an ill-encapsulated code base inherited from
others, I am a big, *big*, BIG fan of encapsulation.
2. One important technique for decoupling different pieces of code is to not
allow any function to have access to the innards of a class unless it really
needs access. Letting it have access means that you have to review the
function definition when changing the class implementation; furthermore,
functions that have access to a class's innards tend to become reliant on
implementation details of the class, and require updating themselves when the
class implementation changes.
3. There are two kinds of functions that have access to the innards of a C++
class: member functions and friend functions. Thus, a function should not be
a member or friend function unless it really needs access to class
implementation details.
4. If the implementation of a class changes and a friend function no longer
requires access to the class innards (you can efficiently implement it on top
of other operations), it is easy to deny the function access to the class
innards -- just remove the friend declaration. This does not change the user
interface and does not break any existing code that uses the class.
5. If the implementation of a class changes and a member function no longer
requires access to the class innards (same reasons), you are now forced to
break encapsulation principle (2). You cannot change the member function into
a non-member function, as this changes the syntax by which it is called and
would break existing code using the class. The member function must remain a
member function, which means that you have to give it access to the class
innards even though it doesn't need this access. Your design is now less
encapsulated than it should be.
> > Once a function is declared as a member function, there's nothing you
> > can do to stop library users from writing x.some_operation() instead of
> > some_operation(x), and hence you cannot later make some_operation()
> > a non-member without breaking existing client code.
>
> 1) Why should you prevent them from writing x.some_operation()?
As described in point 5 above, any such use forces some_operation to remain a
member function, which means that it will forevermore have access to class
implementation details, even if it doesn't need them, resulting in an
encapsulation violation.
> Generic algorithms have a choice for supporting either syntax, and
> often it will be appropriate for them to use the x.some_operation()
> syntax.
The code of the generic algorithm itself should not use the x.some_operation()
syntax, as this limits the algorithm to only be applicable to data types which
are classes providing some_operation() as a member function, and rules out the
possibility of adapting other types for use with the algorithm.
> This means that I find it flatly
> wrong to avoid member functions simply because there might, maybe,
> someday, be a generic algorithm which is going to use a free standing
> method instead.
That's not my reason for avoiding member functions, although it can be a
reason for wanting at least a non-member wrapper function. Points 1-5 above
are my reason for avoiding member functions. Even if you don't think 4 and 5
happen often enough to be important, points 1-3 (a distillation of Scott
Meyers' arguments) are still a strong argument for avoiding member functions
when you don't need access to the implementation details of a class.
> 3) Free functions don't clearly indicate the coupling to the object.
And just what does "coupling to the object" mean? The fact that the object is
being modified? Not all member functions modify the object.
> If it were purely syntax you'd have more of an argument, but it's
> not. In any event, I prefer the x.some_procedure() syntax any way,
> so I'd argue that if you want uniformity we should make everything
> follow this syntax ;).
No, uniformity would argue against using x.some_procedure() for all functions,
as it treats the first argument (x) differently from the others, even when the
arguments are symmetrical. Java's clumsy a.eq(b) (as opposed to a = b or
a == b) is an example.
> So I find it to simply be wrong to
> program the interface assuming a generic usage. It makes the design
> more fragile [...]
>
> 1) Friends are fragile and are open to abuse.
Explain to me how use of friends makes the design fragile. Explain to me how
friends are *more* open to abuse than member functions, as both must be
declared in the class declaration.
> 2) Free functions pollute the namespace.
That's why we have overloading. There may be some implications in the
interaction of member functions vs. non-member functions in C++ overload
resolution that would result in ambiguities for the latter but not for the
former. This would be a valid argument in favor of the member functions, and
my uncertainties on this point are the one reason I am still only considering
the friend-over-member approach. If you have specific examples of where the
non-member syntax would cause problems but the member syntax does not, I would
like to see them.
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk