Boost logo

Boost :

From: williamkempf_at_[hidden]
Date: 2001-11-01 17:55:17


--- In boost_at_y..., "Kevin S. Van Horn" <kevin.vanhorn_at_n...> wrote:
> On Wed, 31 Oct 2001 williamkempf_at_h... 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.

Actually, that's a perversion of the term you're using. Yes, there's
an OO principle that tells you to decouple dependencies when ever
possible, and this leads directly to the "minimal but complete"
principle. But lumping these principles under the guise
of "encapsulation" is awfully misleading.

> 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.

Other than my disagreement with your usage of the term encapsulation
we're in agreement on this one.
 
> 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.

And again I agree with your principles. However, I think you and I
disagree at least somewhat on when methods really need access to
implementation details of the class. This is just a feeling from the
discussion, but I believe you and I disagree about when performance
concerns constitute a need here.
 
> 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.

You're starting to go down the wrong road here by mentioning
friends. I know this only because of previous discussions on this
topic, but your use here is screaming at me.
 
> 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.

This just causes maintenance problems. You could easily find
yourself switching back and forth between friend and non-friend
implementations. However, if you take the approach I've suggested
instead this is a non-issue. You make your best guess as to whether
a method should be free standing or a member, and if you find you
made the wrong choice in the future you simply add the version that's
missing and your maintenance woes are finished with out breaking any
client code.
 
> 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.

Ahh, so that's how your perverting encapsulation. You're taking it
to mean that if a method doesn't need access to implementation
details it simply shouldn't be given access. But that isn't the
purpose of encapsulation. The purpose of encapsulation is to *hide*
the implementation details. You don't know, nor should you care, if
x.some_operation() requires access to internal details of the class.
This allows the class implementer to change the implementation, which
may change whether or not x.some_operation() needs access, with out
any impact to client code and with minimal impact to maintenance.

So, these are the rules I live by.

1) If you need a method requires access to implementation details of
the class make it a member.
2) If you know that a method could be more efficient if it had
access to implementation details of the class make it a member.
3) If you are uncertain if a method will be more efficient if it has
access to implementation details of the class make it a non-member.
4) If you later find that a non-member could be more efficient if it
had access to implementation details of the class add a member and
have the non-member call it.
5) If you find you need a free function in order to work with a
generic algorithm you simply create the adapter.
6) You always avoid friends unless there's no other efficient
solution as they are a kludge to work around encapsulation which lead
to code that's hard to maintain (this is especially true for friend
classes).

> > 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 is simply wrong.

template <typename T>
void call_foo(T o, bar b)
{
   o.foo(b);
}
void foo(bar b);
class foo_adapter
{
public:
   void foo(bar b) { ::foo(b); }
};
call_foo(foo_adapter(), bar());

Adapters freely allow you to change the interface in any way you need
to.

> > 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.

Again, I don't have problems with free standing functions, but *only
when they do not need access to class internals*. What I disagree
with, very strongly, is your choice to use friend functions instead
of member functions. The only reason for such a choice is to allow
for usage with generic algorithms. That's a poor defense for
weakening encapsulation (which is what friend does).

> > 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.

As you yourself pointed out, it's the maintenance coupling that
matters the most. If you change the implementation of a class you
need to know which functions this can impact.
 
> > 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.

Bertrand Meyers takes strong exception to this in OOSC2, and his
arguments are well reasoned and were considered *before* the syntax
was chosen for Eiffel. So at best you're entering into a religious
war here.
 
> > 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.

Fragile because many compilers have problems implementing them and
because the violation of encapsulation causes maintainance problems.
I probably spoke too quickly when I mentioned being open to abuse
because most of the flaws here apply to friend classes and not friend
functions.
 
> > 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.

Overloading does not and can not replace namespace protection
(whether through the namespace keyword or through the more generic
usage of this term that applies to class members). Overloads still
pollute the namespace they are declared in and can easily lead to
ambiguities in numerous ways.

Bill Kempf


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk