S
Steven T. Hatton
The following may strike many of you as just plain silly, but it represents
the kind of delelima I find myself in when trying to make a design
decision. This really is a toy project written for the purpose of learning
to work with C++. It therefore makes some sense for me to give the
situation the amount of consideration presented below. To be quite honest,
I'm amazed at the amount there is to say about such a seemingly simple
situation.
I'm trying to learn to use C++ the way Stroustrup tells us to in TC++(PL).
He has a few different guidelines for determining if something should be
treated as a class or a struct (in the 'C' sense). One measure is whether
it has an invariant that needs to be preserved. So, say I have a rectangle
{x,y,w,h} where x and y are the coordinates of the top left corner, and w
and h are width and height respectively. I can arbitrarily change any
element of that set and still have a rectangle (assuming w > 0, h > 0, or I
assign meaning to the alternatives). Another criteria he has is whether
there are two possible representations of the same abstraction. If so, it
argues that it should be a class To that I say yes.
struct Rectangle{size_t x, y, w, h;};
struct Point{size_t x,y;};
struct RectanglePts{Point xy, dxdy;}
I may also want some convenience functions such as
Rectangle& Rectangle::widen(const size_t dw){ this->w += dw; return this; }
Rectangle& Rectangle::move(const Point dxy){
this->x += dxy.x;
this->y += dxy.y;
return this; }
Interestingly, there is an operation for which when an invariant can be
identified. The displacement between Rectangle::xy and Rectangle::dxdy
should be preserved under translations. Likewise scale operations should
preserve the ratio between the lengths of the sides.
Then I ask if Rectangle should derive from Point. There are arguments for
either choice. He suggests asking the question "can the object have two of
the things you are considering as a base class". At first I might say
"yes", I can have more than two points. I only need two points to define
the rectangle. That argues that the Rectangle should have Point members,
rather than be a Point. But what if we say a point isn't just a point,
it's a location. So we ask again, "can it have two?". Well, yes and no.
It can have many locations within it. Mathematically, there are an
infinite number of points in a rectangle. But the rectangle as a whole
only realistically has one position.
And it makes some sense to treat the rectangle as a position with a size and
shape. Rather than thinking of a Point as a pair of coordinates, or a
location, we might think of it as a geometric object. It could be
considered as the fundamental geometric object. We will never draw a
geometric object on a screen without it having a location. I can use the
same set of operations to move any geometric object, as I use to move a
point. I might even write some operators to add two points as vectors, and
then inherit these in other geometric objects. If I add a Point to a
rectangle it is equivalent to moving the rectangel such that the
coordinates of the top left corner have the components of the point added
to them.
I have not completely definitive test to determine whether a rectangle
should be a class or a struct. Nor can I clearly decide if it should be a
Point and have another Point, or if it should have two points. I favor the
former for the reasons involving positioning.
But now, I have to consider a point. Can it have two representations? A
stupid {x,y} odered pair? Yup! {r cos(theta), r sin(theta)}. I even
briefly entertained the idea of inheriting from std::complex to get my
Point class.
But is it a class? It doesn't seem to have any other invariant than "it has
two components". But that /is/ an invariant that is determined when the
object is created. But that is true of any struct with two members.
So what's the advantage of protecting the components of Point? One argument
people give is that it prevents me from inadvertently modifying the data.
That seems a bit spurious to me in this case. Another, more reasonable
argument that Stroustrup doesn't mention is the notion of event
notification. If I change a value on Point, I may want to notify the
graphics rendering software of the change so it can schedule an update. If
I call a method to make the change, I can have the event fired as a side
effect.
So I make the components protected. So what? What dose that cost me? The
Point that I inherit in Rectangle acts just the way I want it. It inherits
everything from point that I need it to. OTOH, when I try to modify the
components of the lower right corner dxdy, I find I am not permitted to
access the protected data. So, either I use the public interface to modify
the data, or make Rectangle or some standalone function a friend of Point.
The former is a cyclic reference, and bad design, as a general rule. The
latter is something I find to be in bad taste.
If my public interface to point is well designed, writing that bit of extra
code within Rectangle is no big deal. There is one last factor I would
consider, and I honestly don't know what the answer is. Does a decent
compiler (g++, in my case) optimize away the function calls such as:
Point<T>& operator+=(const Point& p)
{
this->x += p.x;
this->y += p.y;
return *this;
}
?
the kind of delelima I find myself in when trying to make a design
decision. This really is a toy project written for the purpose of learning
to work with C++. It therefore makes some sense for me to give the
situation the amount of consideration presented below. To be quite honest,
I'm amazed at the amount there is to say about such a seemingly simple
situation.
I'm trying to learn to use C++ the way Stroustrup tells us to in TC++(PL).
He has a few different guidelines for determining if something should be
treated as a class or a struct (in the 'C' sense). One measure is whether
it has an invariant that needs to be preserved. So, say I have a rectangle
{x,y,w,h} where x and y are the coordinates of the top left corner, and w
and h are width and height respectively. I can arbitrarily change any
element of that set and still have a rectangle (assuming w > 0, h > 0, or I
assign meaning to the alternatives). Another criteria he has is whether
there are two possible representations of the same abstraction. If so, it
argues that it should be a class To that I say yes.
struct Rectangle{size_t x, y, w, h;};
struct Point{size_t x,y;};
struct RectanglePts{Point xy, dxdy;}
I may also want some convenience functions such as
Rectangle& Rectangle::widen(const size_t dw){ this->w += dw; return this; }
Rectangle& Rectangle::move(const Point dxy){
this->x += dxy.x;
this->y += dxy.y;
return this; }
Interestingly, there is an operation for which when an invariant can be
identified. The displacement between Rectangle::xy and Rectangle::dxdy
should be preserved under translations. Likewise scale operations should
preserve the ratio between the lengths of the sides.
Then I ask if Rectangle should derive from Point. There are arguments for
either choice. He suggests asking the question "can the object have two of
the things you are considering as a base class". At first I might say
"yes", I can have more than two points. I only need two points to define
the rectangle. That argues that the Rectangle should have Point members,
rather than be a Point. But what if we say a point isn't just a point,
it's a location. So we ask again, "can it have two?". Well, yes and no.
It can have many locations within it. Mathematically, there are an
infinite number of points in a rectangle. But the rectangle as a whole
only realistically has one position.
And it makes some sense to treat the rectangle as a position with a size and
shape. Rather than thinking of a Point as a pair of coordinates, or a
location, we might think of it as a geometric object. It could be
considered as the fundamental geometric object. We will never draw a
geometric object on a screen without it having a location. I can use the
same set of operations to move any geometric object, as I use to move a
point. I might even write some operators to add two points as vectors, and
then inherit these in other geometric objects. If I add a Point to a
rectangle it is equivalent to moving the rectangel such that the
coordinates of the top left corner have the components of the point added
to them.
I have not completely definitive test to determine whether a rectangle
should be a class or a struct. Nor can I clearly decide if it should be a
Point and have another Point, or if it should have two points. I favor the
former for the reasons involving positioning.
But now, I have to consider a point. Can it have two representations? A
stupid {x,y} odered pair? Yup! {r cos(theta), r sin(theta)}. I even
briefly entertained the idea of inheriting from std::complex to get my
Point class.
But is it a class? It doesn't seem to have any other invariant than "it has
two components". But that /is/ an invariant that is determined when the
object is created. But that is true of any struct with two members.
So what's the advantage of protecting the components of Point? One argument
people give is that it prevents me from inadvertently modifying the data.
That seems a bit spurious to me in this case. Another, more reasonable
argument that Stroustrup doesn't mention is the notion of event
notification. If I change a value on Point, I may want to notify the
graphics rendering software of the change so it can schedule an update. If
I call a method to make the change, I can have the event fired as a side
effect.
So I make the components protected. So what? What dose that cost me? The
Point that I inherit in Rectangle acts just the way I want it. It inherits
everything from point that I need it to. OTOH, when I try to modify the
components of the lower right corner dxdy, I find I am not permitted to
access the protected data. So, either I use the public interface to modify
the data, or make Rectangle or some standalone function a friend of Point.
The former is a cyclic reference, and bad design, as a general rule. The
latter is something I find to be in bad taste.
If my public interface to point is well designed, writing that bit of extra
code within Rectangle is no big deal. There is one last factor I would
consider, and I honestly don't know what the answer is. Does a decent
compiler (g++, in my case) optimize away the function calls such as:
Point<T>& operator+=(const Point& p)
{
this->x += p.x;
this->y += p.y;
return *this;
}
?