Topic: Friend & dual class heirarchies


Author: mfinney@inmind.com
Date: 1995/12/22
Raw View
I keep running into the problem where I have dual class heirarchies
and need to specify friend in one for the other.  However, since
friend is not inherited every time a class is subclassed it is necessary
to specify yet another friend.  For example, suppose I have...

   struct BinaryNode {
      friend BinaryTree;
      friend AvlTree;
      ...};

   struct AvlNode {
      friend AvlTree;
      ...};

   struct BinaryTree {...};
   struct AvlTree : BinaryTree {...};

If I add...

   struct RankedAvlNode : AvlNode {
      friend RankedAvlTree;
      ...};

   struct RankedAvlTree : AvlTree {...};

then it is necessary to add new friend statements to BinaryNode
and to AvlNode.

The problem is that friend is designed to support those situations
where classes are tightly coupled for some reason, such as a
collection and its iterator.  However, with the greater use of
frameworks and design patterns, tightly coupled dual class
heirarchies are becoming more and more common.  Even in the
collection and iterator example, it is more likely to be parallel
class heirarchies rather than simply two classes.

All the usual reasons for protected/private data members and
functions apply as well as the reasons for using friend.  While
I certainly don't think that friend should be used often, its use
is essential for good design in many situations -- especially when
building frameworks.

Does the draft standard address this issue, and if not should a
variant of friend be defined.  Say...

   class BinaryNode {
      friends BinaryTree;
      ...};

would solve the problem quite nicely.  This is a major problem
in keeping a clean design for larger systems.


Michael Lee Finney



---
[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]





Author: John Max Skaller <maxtal@suphys.physics.su.oz.au>
Date: 1995/12/30
Raw View
mfinney@inmind.com wrote:
>
>I keep running into the problem where I have dual class heirarchies
>and need to specify friend in one for the other.

> For example, suppose I have...
>
>   struct BinaryNode {
>      friend BinaryTree;
>      friend AvlTree;
>      ...};
>
>   struct AvlNode {
>      friend AvlTree;
>      ...};
>
>   struct BinaryTree {...};
>   struct AvlTree : BinaryTree {...};
>
>If I add...
>
>   struct RankedAvlNode : AvlNode {
>      friend RankedAvlTree;
>      ...};
>
>   struct RankedAvlTree : AvlTree {...};
>
>then it is necessary to add new friend statements to BinaryNode
>and to AvlNode.
>
>The problem is that friend is designed to support those situations
>where classes are tightly coupled for some reason, such as a
>collection and its iterator.  However, with the greater use of
>frameworks and design patterns, tightly coupled dual class
>heirarchies are becoming more and more common.  .

>All the usual reasons for protected/private data members and
>functions apply ..


I guess the thing to do is be a bit suspicious of the design
technique. For example: why can the user see the node types
above at all?

An idea of how to re-engineer:

  ** Separate the interface and implementation **

The inteface classes don't need friend declarations
because there are no implementation details to be
friendly about :-)

The implementation classes can be structs, with public
data members, and no encapsulation at all. The interface
classes hide the details, but the implementation
classes can access other implementation classes without
friend declarations.

One method of doing this is to use indirection: a private
pointer to a struct. The user can pervert any
data they can lay their hands on .. but they can't
get their hands on it!

I have a similar kind of problem: I have several
kinds of invasive reference counting smart pointers: they
need access to a refcnt data member of the object
they manage. I make it public.

Now, this means the user can screw up the automatic
memory management. Is this bad?

I think it's the wrong question. The user has _asked_
a server (the smart pointer) to manage an object
_they_ created. Before that, they could already screw
up the memory management. By handing over to the smart
pointer, the user is asking for help -- the smart
pointer promises to help _provided_ the user doesn't
go and do something stupid (like manually delete the object).

But the user can still intervene and take control
back ... and may actually _need_ to fiddle the
reference count. For example .. by inventing
another kind of reference counting smart pointer!

With several kinds of smart pointers
the user may decide to change the management protocol
and may need to manually adjust the ref count
in the process of transfering responsibility.

The refcount could be made private by making the
smart pointer a friend. But then only the nominated
kind of smart pointer can manage the object.

I HAVE caused a crash by messing up invasive
smart pointers. But I didn't do it by writing
to the publically accessible refcount, I did it
by stealing the raw pointer and giving it
to a _non-invasive_ reference counted pointer --
which thought it had exlusive control and deleted
the object from under the nose of the counting
smart pointers.

In my opinion, encapsulation of objects is not quite right:
data structures and functions in an implemtation
module should have free access to other components
of the module. [Naturally, the user interface provides
only managed access].

That is, C++ and other "object" oriented systems
operate by determining that the granularity
of the module notion is the class -- which is too
fine. C++ provides friends to escape this design
compromise (which seems necessary for linear
polymorphism).

This suggests working with structs and global functions
in a space without any protection, (simulating
a module of coarser granularity) and then hiding
the implementation of the _system_ behind an
interface -- which may look nothing like the
representation.

If you consider STL, it is very much an unprotected,
unencapsulated system residing in the implementation domain.
To use it, it needs to be hidden
inside encapsulated objects of the problem domain.

So it may pay to be a bit sloppier about protection
of data structures .. since they're largely tools
for creating representations of abstractions,
and it is _these_ representations that need to be hidden,
rather than the details of the tools used to create them.

For example: if you have objects that should NOT be moved,
then you might put them in a list<>, which promises never to
move things. You would frown on a vector<>, because it
does tend to move things.

..tend to. But sometimes, you can reserve storage
in the vector, and then objects won't move. You can
rely on it if you know the implementation details.

--
John Max Skaller               voice: 61-2-566-2189
81 Glebe Point Rd              fax:   61-2-660-0850
GLEBE NSW 2037                 web: http://www.maxtal.com.au/~skaller/
AUSTRALIA                      email: skaller@maxtal.com.au
---
[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]





Author: mfinney@inmind.com
Date: 1995/12/30
Raw View
In <4c17cf$k52@oznet03.ozemail.com.au>, John Max Skaller <maxtal@suphys.physics
.su.oz.au> writes:
>I guess the thing to do is be a bit suspicious of the design
>technique. For example: why can the user see the node types
>above at all?

In this particular example, these are low level data structures
which are used to implement higher level data structures.  Those
data structures hide everything.  But due to efficiency concerns,
using indirected pointers is not acceptable (actually, the node
classes are not even virtual for space concerns).  The node
structure is intentionally exposed (since this is a low-level
data structure) but only operations which can VIEW the structure
are publically available.  These operations are used by higher
level code to create iterators, more sophisticated data structures,
etc..

But that code has to see a certain level of detail.  These structures
are sufficiently strong that they can also be used directly by the
user.  The tradeoff is that one type of tree can be substituted for
another type of tree, but not for, say, a hash table.  Where using
the high level structures will allow substitution by a hash table,
but at a minor cost of efficiency (such as engendered by use of
indirection for encapsulation).  Most applications can afford such
costs, but some cannot.  So you need both types of data structures.

But, even with the "high" level data structures, this type of
problem repeats itself in the dual Collection/Iterator heirarchy.

>An idea of how to re-engineer:
>
>  ** Separate the interface and implementation **

As mentioned above, unacceptable due to efficiency considerations.

>I have a similar kind of problem: I have several
>kinds of invasive reference counting smart pointers: they
>need access to a refcnt data member of the object
>they manage. I make it public.
>
>Now, this means the user can screw up the automatic
>memory management. Is this bad?

Yes.  If your "smart" pointers are derived from a common
class then you do have the same problem.  You need to
declare it and its subclasses to be friends in a single
declaration point and to make that data element private.
Ideally, you would export JUST that single data element,
but friendship is, unfortunately, not sufficiently fine-grained
for that.

>I think it's the wrong question. The user has _asked_
>a server (the smart pointer) to manage an object
>_they_ created. Before that, they could already screw
>up the memory management. By handing over to the smart
>pointer, the user is asking for help -- the smart
>pointer promises to help _provided_ the user doesn't
>go and do something stupid (like manually delete the object).

In a small system, perhaps that attitude is o.k. but in a
larger system the need to write code "protectively" will
disallow that approach.

>But the user can still intervene and take control
>back ... and may actually _need_ to fiddle the
>reference count. For example .. by inventing
>another kind of reference counting smart pointer!

If that need is actually anticipated, then the data element
should be protected instead of private.  Or you have the
situation where you need another "friend".

The fundamental issue is that while ideally a class design
can be fully decomposed, there are any number of examples
which REQUIRE less than full decomposition.  And the C++ way
of providing for that is "friend".  Some of those examples fall
into the area where just a single unction needs to be a friend.
And, barring mutually dependent definitions, C++ allows for
that.  There are cases where just a single class needs to be
a friend.  And C++ allows for that.  But there are also cases,
especially when building frameworks, where an entire class
heirarchy needs to be a friend.  And C++ does not allow for
that.

Which is why I suggested that "friends" be introduced to allow
for the lack.  I would ALSO like to see friendship be more fine
grained so that I can export just a particular data element at
the particular privledge level that I desire.  Perhaps I don't
want EVERYTHING to be shared, just a particular component.
Perhaps I don't want the private information to be inherited
if using a "friends" statement.

>In my opinion, encapsulation of objects is not quite right:
>data structures and functions in an implemtation
>module should have free access to other components
>of the module. [Naturally, the user interface provides
>only managed access].

I disagree with this.

>That is, C++ and other "object" oriented systems
>operate by determining that the granularity
>of the module notion is the class -- which is too
>fine. C++ provides friends to escape this design
>compromise (which seems necessary for linear
>polymorphism).

I will admit that a "larger" encapsulation mechanism could be
useful.  But even within such a mechanism I would want to
use friendship to carefully dole out the ability to interact.

>So it may pay to be a bit sloppier about protection
>of data structures .. since they're largely tools
>for creating representations of abstractions,
>and it is _these_ representations that need to be hidden,
>rather than the details of the tools used to create them.

Again, I disagree here.  I want the strongest encapsulation I
can manage to strictly control access to data element and
methods.


Michael Lee Finney
---
[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]