A simple way to get started on the principles behind writing C++ libraries by seeing how to get rid of all of those accessor functions; all to the tune of Video Killed the Radio Star¹ [1Of course the meter doesn't quite work, but it's close.].
One of the basic rules of encapsulation² [2Why encapsulation is a Good Thing™ is beyond the scope of this article.] is that we should hide the implementation of our classes. We define the interfaces and not the data structures so that we are free to alter our implementations without having to worry quite so much about how this will affect the client code.
This leads to a style of implementation where every attribute ends up with get
and set
accessor functions. In turn this leads to classes that look a lot like this:
class GeoPosition { public: GeoPosition(); GeoPosition( double lat, double lon ); double getLat() const; void setLat( double newValue ); double getLon() const; void setLon( double newValue ); private: double lat, lon; };
I'm sure that I don't need to labour the point by showing the blindingly obvious implementation here. Compare this with the following code:
class GeoPosition { public: GeoPosition(); GeoPosition( double lat, double lon ); double lat, lon; };
That's a huge saving in the declaration alone. The implementation has just shortened by no small order either. The problem is that it breaks the encapsulation. If we ever need to do anything more complex with the attribute we will have to re-write a lot of client code which is going to be very time consuming. What sort of thing are we likely to want? The obvious thing is a range check.
Why the range check? Well, both latitudes and longitudes are constrained to a range of ±180°³ [3There are lots of reasons why you may not actually want to constrain them in an implementation, but for our purposes we'll go with this specification.Of course the best reason not to constrain them like this was pointed out to me on Reddit. The longitude is ±180°, but the latitude is of course ±90°. I will update this eventually with an example that does work and still illustrates the point I'm trying to make. In the meantime I hope you bear with my idiocy.]. This means that our accessors aren't quite as brain dead as we might first imagine. The set
members should really look something like this⁴ [4In production code I wouldn't use this exception. Consider this an illustration.]:
void GeoPosition::setLat( double nLat ) { if ( nLat < -180. || nLat >= 180. ) throw std::out_of_range( "New latitude is out of allowed range of +-180 degrees" ); lat = nLat; }
Note that I've followed the standard C++ convention of allowing the lower bound but excluding the upper bound. The get
is still fairly brain-dead though, looking like this:
double GeoPosition::getLat() const { return lat; }
The first thing we're going to do is to change the accessors so that they're slightly more idiomatic. Because we can overload the same name with different parameters in C++ we can just chop the get
and set
from the names. This gives us a class that looks like this:
class GeoPosition { public: GeoPosition(); GeoPosition( double lat, double lon ); double lat() const; void lat( double newValue ); double lon() const; void lon( double newValue ); private: double m_lat, m_lon; };
This means the we can now replace anything that looks like:
place.setLat( place.getLat() + 10. ); return place.getLat();
With this:
place.lat( place.lat() + 10. ); return place.lat();
It may seem like a small matter, but again it saves us some typing. More importantly it also gives us a lot more flexibility on how to implement the attribute which at the end of the day is one of the reasons for using encapsulation.
Even if you don't read any further than this you should take away that in C++ you should never use get
and set
in accessor names because it limits what you can do later on. What those things are is what the rest of this article is about.
We found that we could get rid of the get
and set
parts of the names, but there's still a lot of boring repetitive code to write. Boring and repetitive tasks is one of the things that classes help us to deal with so maybe we can write a class to handle it all for us.
Let's see what happens if we implement the attribute as a class.
class Latitude { public: Latitude(); Latitude( double ); double get() const { return m_lat; } void set( double nLat ) { if ( nLat < -180. || nLat >= 180. ) throw std::out_of_range( "New latitude is outside of allowed range of +-180 degrees" ); m_lat = nLat; } private: double m_lat; };
Hmmm… What was I going to call the accessors? I can't leave them with no name so I've brought back in get
and set
. Not ideal.
It turns out that C++ provides us a way of not giving them a name at all. We can actually overload the ()
operator⁵ [5Classes which implement this operator are normally called functor classes because instances of them can be used with the same syntax as function calls.]. Now this sounds odd and in a way it is, but the reason that it's there is for exactly this sort of eventuality. We overload an operator by defining a method name of operator X
where X
is the operator. For our purposes we want ()
so this gives us operator ()
.
Now watch carefully when we use it though. The syntax is pretty obvious when you think about it, but looks really strange at first.
class Latitude { public: Latitude(); Latitude( double ); double operator ()() const { return m_lat; } void operator ()( double nLat ) { if ( nLat < -180. || nLat >= 180. ) throw std::out_of_range( "New latitude is outside of allowed range of +-180 degrees" ); m_lat = nLat; } private: double m_lat; };
If you think about you'll see that the first ()
is the operator name and the second set are for the operator's parameters. The first one of course doesn't have any parameters⁶ [6We could have written operator()( void )
if we'd wanted to, but the void
is optional and normally left out in C++.] and the second one takes a double. Just to double underline this (ahem), I've written the members as prototypes with the accessor names in bold below:
double operator ()() const; void operator ()( double nLat );
The whole operator ()
is the name of the function.
We can now use this in our first class like this:
class GeoPosition { public: GeoPosition(); GeoPosition( double lat, double lon ); Latitude lat; double lon() const; void lon( double newValue ); private: double m_lon; };
Note that I've only changed the latitude member so far. To manipulate it we will have code that looks like this:
place.lat( place.lat() + 10. ); return place.lat();
Notice that this looks exactly like the accessors we had earlier after we removed the get
and set
. This is the first part of the reason why leaving the get
and set
out of accessor names is a pretty good idea. It gives us the possibility of switching between writing accessor members in a class and using a seperate helper class to implement them⁷ [7You may need to think a little about this. The reason is that if we use the get
and set
in the names then we will be trying to replace getLat
and setLat
which isn't possible. Try it and you'll see what I mean.].
We can imagine that we can of course do exactly the same thing with a class Longitude
which would reduce the size of our class even more, but we don't actually have to. Looking at the class you can see that it doesn't really need to be limited to latitude at all. If we rename it we can use it for both:
class SphericalCoordinate { public: SphericalCoordinate(); SphericalCoordinate( double ); double operator ()() const { return m_coord; } void operator ()( double nCoord ) { if ( nCoord < -180. || nCoord >= 180. ) throw std::out_of_range( "New co-ordinate value is outside of allowed range of +-180 degrees" ); m_coord = nCoord; } private: double m_coord; };
Just by renaming it we have something that is suitable for use as either latitude or longitude.
class GeoPosition { public: GeoPosition(); GeoPosition( double lat, double lon ); SphericalCoordinate lat, lon; };
This is clearly much better:
If you stop reading now you have a technique that you can use to help you write classes quicker and with less errors. But we can do even better.
What is a template and what is it meant to do for us? A template is a way to write a class so that we can configure some things that we would otherwise have to specify in our code. What do we mean?
In our class we've decided that we want our co-ordinate to be stored as a double
. This is fine, but what if there are some places that we need to store the location as an int
and other places where we need to store it as a double
. Do we need to write two versions of our class? Do we need a SphericalCoordinateInt
and a SphericalCoordinateDouble
?
It's exactly this that the templates do for us. We can write the class without having to decide what type we use to store the actual co-ordinate. The syntax is actually fairly straightforward. To change the class into a template we simply tell it that it is going to be a template and tell it that we want to specify a type to use. The first part of the class declaration changes to this:
template< typename t_coordinate > class SphericalCoordinate ...
Now we just need to replace the occurances of double
with t_coordinate
in our complete implementation:
template< typename t_coordinate > class SphericalCoordinate { public: SphericalCoordinate(); SphericalCoordinate( t_coordinate ); t_coordinate operator ()() const { return m_coord; } void operator ()( t_coordinate nCoord ) { if ( nCoord < -180. || nCoord >= 180. ) throw std::out_of_range( "New co-ordinate value is outside of allowed range of +-180 degrees" ); m_coord = nCoord; } private: t_coordinate m_coord; };
In order to use it in our class we now have:
class GeoPosition { public: GeoPosition(); GeoPosition( double lat, double lon ); SphericalCoordinate< double > lat, lon; };
This now looks a little more complex, but actually it has one important benefit over the previous version—the precision of the latitude and longitude are now explicitly stated in our GeoPosition
class and can just as easily be changed there⁸ [8I'm guessing that you will also notice that we could templatise GeoPosition
too; and in some situations that may be the right thing to do.].
This last point is more important than it at first seems. When somebody else is going through this code the fact that the actual underlying type is in GeoPosition
will make it easier for them to understand the class. This means that although we have used much more complex techniques the code is actually easier to maintain. And even better, because we're not writing the same accessors time and again we have something that is much more likely to be correct.
I'm going to just bullet these out. There are some other considerations that you should be aware of, but nothing that should stop you from using the technique:
void operator()( t_coordinate )
in our case) at the moment doesn't return anything. There is a good argument that it should return the new value (or even the old value). This depends on the context that it is being used in though. If you suspect that there is any chance of confusion between the return value being the old or the new one then don't do it. The principle of least surprise would to me intimate that it should return the new value, but what one person finds surprising is often very different to what another finds surprising.UPDATE
or INSERT
statements or even WHERE
clauses (for example adding single quotes where appropriate).BSTR
or VARIANT
).We've seen what we should call accessors and why. We've also seen that we can use helper classes to implement the attributes which reduces the amount of code we need to write which is good for all sorts of reasons. We've also seen that we can use some simple template syntax to start us to get used to them and to put control of the types closest to where they're needed.
As ever there is an interesting discussion on Reddit.