Accelerated C++ Chapter 15: Would you do that?

S

Steven T. Hatton

The 15th chapter of Accelerated C++ develops a rather lengthy, complex, and
(IMO) unusual example which seems to, on a small scale, defy many of the
intuitive notions I have about OOP. There are empty public interfaces to
most of the classes, and almost all of the functions in the interfaces are
private. The authors do this, they say, to hide the implementation details
from the user. They don't want the user of the classes to access their
functionality in any way except through what I might call a "wrapper" or a
"proxy" (I think I'm in agreement with GoF here). Add to that, the
operations on the hidden objects are performed by namespace local
functions. The authors actually call them "global", but I won't go that
far. They grant access to all the various parts to functions and classes
that need it by declaring function and classes as friends of others. Many
of these friendship relations are mutual.

You can find the code here: http://www.acceleratedcpp.com/

The example is very challenging to my sense of "proper" design. On the
small scale it seems very interdependent, and inflexible. OTOH, it
provides a lot of functionality to the user while hiding virtually all of
the complexity of the implementation.

Has anyone else read that chapter and formed an opinion about that design
approach?
--
"If our hypothesis is about anything and not about some one or more
particular things, then our deductions constitute mathematics. Thus
mathematics may be defined as the subject in which we never know what we
are talking about, nor whether what we are saying is true." - Bertrand
Russell
 
A

Alf P. Steinbach

* Steven T. Hatton:
The 15th chapter of Accelerated C++ develops a rather lengthy, complex, and
(IMO) unusual example which seems to, on a small scale, defy many of the
intuitive notions I have about OOP.

...

You can find the code here: http://www.acceleratedcpp.com/

The example is very challenging to my sense of "proper" design. On the
small scale it seems very interdependent, and inflexible. OTOH, it
provides a lot of functionality to the user while hiding virtually all of
the complexity of the implementation.

Has anyone else read that chapter and formed an opinion about that design
approach?

I don't have the book, but judging from the code it's an example of a
design pattern. This design pattern allows you to assign Picture objects
that are just wrappers for inner objects of various types, which you as a
class user don't need to know about. The representation of an object is
thus changed at run-time, and that can be used to impose constraints (e.g.
what function calls are valid), optimize based on state, whatever.

The concrete example could have been implemented much more clearly and with
much less code without using the design pattern: that doesn't mean that the
authors have chosen an inappropriate design, but that they have chosen an
appropriately small example that doesn't get in much in the way.

Regarding the code I find it disconcerting that the authors (Andrew if
you're reading this please explain) have chosen to let Pic_base yield
'friend'-ship to its derived classes instead of using the C++ mechanism,
'protected'. But perhaps 'protected' has not yet been introduced in the
book, for e.g. pedagogical or space reasons. Perhaps 'protected' would come
in chapter 16 or later, a volume II?

If not then there is one property of 'friend' that differs greatly from
'protected', namely that unless the derived class itself takes measures to
let its derived classes have access, with 'friend' the access stops at the
first level of derived class.

But I can't figure out why that would be a Good Idea -- if it is.
 
S

Steven T. Hatton

Alf said:
* Steven T. Hatton:


If not then there is one property of 'friend' that differs greatly from
'protected', namely that unless the derived class itself takes measures to
let its derived classes have access, with 'friend' the access stops at the
first level of derived class.

But I can't figure out why that would be a Good Idea -- if it is.
They do explain the reasoning for that unusual element of the design. I'll
let Andrew decided if he wants you to read the book to find out.
--
"If our hypothesis is about anything and not about some one or more
particular things, then our deductions constitute mathematics. Thus
mathematics may be defined as the subject in which we never know what we
are talking about, nor whether what we are saying is true." - Bertrand
Russell
 
A

Andrew Koenig

Regarding the code I find it disconcerting that the authors (Andrew if
you're reading this please explain) have chosen to let Pic_base yield
'friend'-ship to its derived classes instead of using the C++ mechanism,
'protected'. But perhaps 'protected' has not yet been introduced in the
book, for e.g. pedagogical or space reasons. Perhaps 'protected' would
come
in chapter 16 or later, a volume II?

If I remember correctly, here's the problem:

class Pic_base {
// ...
};

class Hcat_pic: public Pic_base {
// ...
Ptr<Pic_base> left, right;
// ...
};

Here, Ptr is a kind of smart pointer, so that (for example) if h is an
object of type Hcat_pic, executing h.left->width() calls the width memver of
the object to which h.left is attached--assuming that protection will let
you do so.

Now consider how the C++ "protected" mechanism works. It allows a member of
a derived class to access the base-class parts of derived-class objects.
For example:

class B {
protected:
void foo();
};

class D: public B {
public:
void bar() {
foo();
this->foo();
B* bp = new D; bp->foo();
D* dp = new D; dp->foo();
}
};

Inside D::bar, the call to foo() is OK, as is the call to this->foo(). In
both cases, a member of D is accessing a protected member of B that is the B
part of a D object.

In the next line, bp points to a D object. However, the static type of bp
is B*, and that means that D is attempting to access a protected member of
an object that is not of class D. Accordingly, this line is a compile-time
error.

The last line is OK -- dp's static type proves that it must point to a D
object, so it is OK to call a protected member of the B part.

The reason for this behavior is that otherwise, anyone could access any
protected member of any object: Merely derive a dummy class from the class
with the member you wish to access, and put the code that wants to access it
in a member of your dummy class.

Now consider a member of Hcat_pic that wishes to call this->left->mem(). As
far as the compiler knows, this->left is a smart pointer that refers to an
object of class Pic_base. Therefore, any code I might write cannot take
advantage of being part of a member of a class derived from Pic_base,
because being a member of a derived class is not enough to grant access to
what, as far as the compiler knows, is an arbitrary base-class object.

That's why I couldn't use the protected mechanism in this example. It's a
nuisance, but there you are.
 
A

Alf P. Steinbach

* Andrew Koenig:
* Alf P. Steinbach:


If I remember correctly, here's the problem:

class Pic_base {
// ...
};

class Hcat_pic: public Pic_base {
// ...
Ptr<Pic_base> left, right;
// ...
}; ....

Now consider a member of Hcat_pic that wishes to call this->left->mem(). As
far as the compiler knows, this->left is a smart pointer that refers to an
object of class Pic_base. Therefore, any code I might write cannot take
advantage of being part of a member of a class derived from Pic_base,
because being a member of a derived class is not enough to grant access to
what, as far as the compiler knows, is an arbitrary base-class object.

That's why I couldn't use the protected mechanism in this example. It's a
nuisance, but there you are.

Thank you.

I was afraid the answer would be "to effectively limit the set of derived
classes to those specified as friends in Pic_base". ;-)

When that is not the goal it's not just a little nuisance, it's a
maintenance problem where everything that depends on Pic_base has to be
rebuilt when a new derived class is added.

One solution is to make the thorny members 'public', and in this particular
example I think that would actually be good idea.

Otherwise, general case, 'protected' can be used via simple wrappers, like
(atual code)


===========================================================================
class Pic_base {
friend std::eek:stream& operator<<(std::eek:stream&, const Picture&);

protected:
// no `public' interface (except for the destructor)
typedef std::vector<std::string>::size_type ht_sz;
typedef std::string::size_type wd_sz;

// this class is an abstract base class
virtual wd_sz width() const = 0;
virtual ht_sz height() const = 0;
virtual void display(std::eek:stream&, ht_sz, bool) const = 0;

static void pad(std::eek:stream& os, wd_sz, wd_sz);

static wd_sz width_of( Pic_base const& pic ) { return pic.width(); }
static ht_sz height_of( Pic_base const& pic ) { return pic.height(); }
static void display(
Pic_base const& pic, std::eek:stream& s, ht_sz size, bool pad
)
{
pic.display( s, size, pad );
}

public:
virtual ~Pic_base() { }
};
===========================================================================

Gain: Pic_base is no longer a maintenance problem wrt. derived classes.
Cost: Almost zero.

And yes, I compiled & tested the whole thing (using 'main1.cpp'), with use
of the problematic member functions rewritten to use the static wrappers.

I have one further suggestion, and that is to _not_ use logically different
types for vertical and horizontal sizes.

But perhaps I've misunderstood that.

Anyway, hope this can be useful.


Cheers,

- Alf (master of 'protected', heh heh!)
 
S

Steven T. Hatton

Alf said:
* Andrew Koenig:

Thank you.

I was afraid the answer would be "to effectively limit the set of derived
classes to those specified as friends in Pic_base". ;-)

When that is not the goal it's not just a little nuisance, it's a
maintenance problem where everything that depends on Pic_base has to be
rebuilt when a new derived class is added.

OTOH, the design limits those dependencies to those specified as friends of
Pic_base,
One solution is to make the thorny members 'public', and in this
particular example I think that would actually be good idea.

Otherwise, general case, 'protected' can be used via simple wrappers, like
(atual code)


===========================================================================
class Pic_base {
friend std::eek:stream& operator<<(std::eek:stream&, const Picture&);

protected:
// no `public' interface (except for the destructor)
typedef std::vector<std::string>::size_type ht_sz;
typedef std::string::size_type wd_sz;

// this class is an abstract base class
virtual wd_sz width() const = 0;
virtual ht_sz height() const = 0;
virtual void display(std::eek:stream&, ht_sz, bool) const = 0;

static void pad(std::eek:stream& os, wd_sz, wd_sz);

static wd_sz width_of( Pic_base const& pic ) { return pic.width();
}
static ht_sz height_of( Pic_base const& pic ) { return pic.height();
} static void display(
Pic_base const& pic, std::eek:stream& s, ht_sz size, bool pad
)
{
pic.display( s, size, pad );
}

public:
virtual ~Pic_base() { }
};

I didn't think of doing that here. I have used that approach with builder
functions that assemble recursive data structures consisting of instances
of classes derived from a common base. (Which is more or less what we are
doing.) It does seem more attractive to my way of thinking than is the
extensive use of friend specifiers. I have neither tried this, nor
finished my first cup of coffee, so I may be wrong, but I believe you could
move the "global" functions into the baseclass as static members. Then
invoke those through a function exposed on the Pic interface. The friend
relations could be limited to specifying friend class Pic; in Pic_base.

Andrew and Barbara didn't like having the user write pic.frame(); because
they didn't like forcing the user to write things such as
pic.frame().hcat(pic). They found hcat(frame(pic), pic) to be clearer. My
suggestion would force the user to write things such as
Pic::hcat(Pic::frame(pic),pic), which isn't all that pretty.

Too bad that (AFAIK), the language does not support automatic name
resolution that would resolve that cumbersome expression from
hcat(frame(pic),pic). That is, automatically try static members of the
arguments for a matching function call. We might then be able to write
things such as `static Vector3 Vector3::eek:perator+(const Vector3& lhs, const
Vector3& rhs);' and use it like `Vector3 v = v1 + v2;'. There's probably a
good reason the language doesn't support that functionality, but I'm not
seeing it right now.

I really don't like using the friend specifier. For example, I prefer
struct MyClass{/*...*/std::eek:stream& print(std::eek:stream& out ) const
{/*...*/}}; std::eek:stream& operator<<(std::eek:stream& out, const MyClass& mc
{return mc.print(out);}

Yes, it's more verbose, but it is not as tightly coupled as the solution of
using friend std::eek:stream& operator<<(std::eek:stream&, const MyClass&);. I
could do something similar with Pic and a namespace local function that
invokes the cumbersome static member for the user.
--
"If our hypothesis is about anything and not about some one or more
particular things, then our deductions constitute mathematics. Thus
mathematics may be defined as the subject in which we never know what we
are talking about, nor whether what we are saying is true." - Bertrand
Russell
 
A

Alf P. Steinbach

* Steven T. Hatton:
Alf P. Steinbach:

I have neither tried this, nor
finished my first cup of coffee, so I may be wrong, but I believe you could
move the "global" functions into the baseclass as static members.

?

In the code I posted there were no global or "global" functions.

There were some baseclass static members.
 
S

Steven T. Hatton

Alf said:
* Steven T. Hatton:

?

In the code I posted there were no global or "global" functions.

There were some baseclass static members.

Yes, but you didn't implement all the functionality either. You did almost
what I was suggesting with operator<<. I would have exposed the call to
display() through the Pic interface. Perhaps that adds needless
complexity, but it restricts everything the user does with Pic to its
interface, rather than subtelly exposing it through a friend declaration in
Pic_base. If you really don't want users calling Pic::display(), make it
protected or private, and declare operator<< to be a friend of Pic.

--
"If our hypothesis is about anything and not about some one or more
particular things, then our deductions constitute mathematics. Thus
mathematics may be defined as the subject in which we never know what we
are talking about, nor whether what we are saying is true." - Bertrand
Russell
 
A

Alf P. Steinbach

* Steven T. Hatton:
Correction: operator<<.

Yes, but you didn't implement all the functionality either.

?

AFAIK I did.

It compiled, and worked.

You did almost
what I was suggesting with operator<<. I would have exposed the call to
display() through the Pic interface. Perhaps that adds needless
complexity, but it restricts everything the user does with Pic to its
interface, rather than subtelly exposing it through a friend declaration in
Pic_base. If you really don't want users calling Pic::display(), make it
protected or private, and declare operator<< to be a friend of Pic.

?

Assuming "Pic" means "Pic_base", that's the code I presented.

Regarding the word "interface", there are many meanings, and one common one
includes 'friend' functions.
 
S

Steven T. Hatton

Alf said:
* Steven T. Hatton:

Correction: operator<<.



?

AFAIK I did.

It compiled, and worked.

Yes, but that, AFAIK left the friend declarations in all the derived classes
of Pic_base. One problem I have with that is that I cannot look at the
definition of, say, String_Pic, and determine what Picture might be using
from it. In order to modify String_Pic, I also have to examine closely
what Picture is doing with it. I really don't see a clean way around the
use of friend declarations in the Pic_Base derivatives which doesn't
involve making part of their interface public. I believe all that would
need to be exposed are the constructors.
?

Assuming "Pic" means "Pic_base", that's the code I presented.

I really did mean Pic, but I should have written Picture.
Regarding the word "interface", there are many meanings, and one common
one includes 'friend' functions.

I'm following Andrew and Barbara on calling the Picture class the user
interface. What I'm saying is, the Picture class should provide the only
exposure of Pic_base and its derivatives to the outside world.

--
"If our hypothesis is about anything and not about some one or more
particular things, then our deductions constitute mathematics. Thus
mathematics may be defined as the subject in which we never know what we
are talking about, nor whether what we are saying is true." - Bertrand
Russell
 
A

Alf P. Steinbach

* Steven T. Hatton:
* Alf P. Steinbach:

Yes, but that, AFAIK left the friend declarations in all the derived classes
of Pic_base.

"You didn't solve it all! -Huh? -Yes, although you did what you said
you did regarding those shipping schedules, you didn't guarantee eternal
world peace and explain the mystery of dark matter! So there!" :)

One problem I have with that is that I cannot look at the
definition of, say, String_Pic, and determine what Picture might be using
from it. In order to modify String_Pic, I also have to examine closely
what Picture is doing with it. I really don't see a clean way around the
use of friend declarations in the Pic_Base derivatives which doesn't
involve making part of their interface public. I believe all that would
need to be exposed are the constructors.

Yes, with one proviso: those classes should be moved out of the header
file (they don't belong there, as they can't be used by client code), and
down in the implementation file, in an anonymous namespace there.

Or possibly, when regarding this as a kind of "in principle" code that
merely exemplifies how to do things, to separate logical modules,

Merely exposing the constructors without moving the classes would give
the client code functionality that shouldn't be given.

Moving them and making the constructor 'public' gets rid of all those
'friend' backlinks -- and yes, I did that for one of those classes
just to be sure I'm not stating gut-feelings as facts.

It also makes for a _much_ cleaner header file.

I'm following Andrew and Barbara on calling the Picture class the user
interface. What I'm saying is, the Picture class should provide the only
exposure of Pic_base and its derivatives to the outside world.

Yes.
 
A

Alf P. Steinbach

* Steven T. Hatton:
[Not satisfied with "Accelerated C++" ch. 15 design/code]

Summing up, how to clean up the "Accelerated C++" ch. 15 code:

* Where 'Pic_base' declares derived classes as 'friend's:
- Replace with protected wrapper functions.

* Where 'Pic_base' derived classes declare global funcs as 'friend':
- Move those derived classes to the implementation file, make
constructors public.

* Where the header file has 'using std::max;':
- Remove that abomination.

* Where different logical types are used for heights and widths:
- (Untested) Replace with a single type.

* Where the std::vector<std::string> type is specified again and again:
- Replace with a typedef.

Also, minor point, I'd like the #include of "Ptr.h" to be moved to the
top, to be more confident that it in turn includes all that it needs.
It's a good idea to do that in general, I think. But this is preference.
 
S

Steven T. Hatton

Alf said:
* Steven T. Hatton:
[Not satisfied with "Accelerated C++" ch. 15 design/code]

Summing up, how to clean up the "Accelerated C++" ch. 15 code:

* Where 'Pic_base' declares derived classes as 'friend's:
- Replace with protected wrapper functions.

* Where 'Pic_base' derived classes declare global funcs as 'friend':
- Move those derived classes to the implementation file, make
constructors public.

Some people - especially in the Java community - believe constructors are
evil, and that everything that is exposed to the user should be through a
factory pattern. I haven't gotten very far in the GoF Design Patterns book
yet, but they have already hinted at that approach. Personally, I find it
to often be quite a bother to use factories. Sometimes they make a lot of
sense. Perhaps this is a situation where it would make sense.
* Where the header file has 'using std::max;':
- Remove that abomination.

Well, yeh.
* Where different logical types are used for heights and widths:
- (Untested) Replace with a single type.

* Where the std::vector<std::string> type is specified again and again:
- Replace with a typedef.

Also, minor point, I'd like the #include of "Ptr.h" to be moved to the
top, to be more confident that it in turn includes all that it needs.
It's a good idea to do that in general, I think. But this is preference.

I was actually quite surprised to see that, and fully agree that #includes
should be at the top of the file (or replaced by superior technology -
TBD).

Overall, I think the book is very good. Even though I read TC++PL(SE),
there are many concepts I have yet to fully grasp. AC++ takes a more
pedagogical approach, and in some ways was boringly elementary. OTOH, I
can say that I gained some element of new insight from just about every
page of the book. Even though the design of the final major example
(Chapter 15) is not, IMO, ideal for production code, it did serve to give
me better insight into how virtual inheritance works, and how one might use
friend specifications (though I have yet to be persuaded that the friend
specifier is a good idea.) The general concept of providing a simple smart
pointer interface to a complex implementation is certainly a valuable
design approach. They introduced much of that in Chapter 14, but developed
further in Chapter 15.

I'm wondering how many decades it will take me to master this language. The
one thing I am confident about is that if I continue to program in C++ for
the next 10 years, I will still hate the CPP.
--
"If our hypothesis is about anything and not about some one or more
particular things, then our deductions constitute mathematics. Thus
mathematics may be defined as the subject in which we never know what we
are talking about, nor whether what we are saying is true." - Bertrand
Russell
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,581
Members
45,057
Latest member
KetoBeezACVGummies

Latest Threads

Top