Programming in a object oriented language can be seen as an exercise in extending the type system. And if all your code is wrapped nicely in classes and functions, what’s left is just combining those using the language. Simple, right?
Seen from this viewpoint, the importance of designing your types correctly become very important. And the best way to design them correctly, is to have them behave as much as possible as the built-in types and library types. (On a side note, this is one reason I dislike Java’s lack of operator overloading.)
As an example, say I am designing an embedded system for a car stereo. Every radio-station is stored in a RadioStation
class. There is also a RadioStationContainer
class that manages the radiostations. Now we need a function to add RadioStation
s to the container. What do we name it? What name will make a good interface for the user of this library? addRadioStation()
?
I would say a much better name is push_back()
. Even though you might think addRadioStation()
sounds like a more intuitive name, if you are making a container, I’d argue having it behave like all other containers is more intuitive.
How about allowing people to iterate over radio stations? The iterator type will depend on the type of container RadioStationContainer
is using internally. One method I’ve seen is people use something like this (oustide the RadioStationContainer
class): typedef std::list<RadioStation> RSCit
. This gives people a short an easy name for the iterator, right? Again I would argue you should instead make a normal typedef inside the class, so people can use the normal RadioStationContainer::iterator
. If they need a shorthand, they can make their own typedef.
Here is an example of a RadioStationContainer
that can be used as a normal container:
class RadioStationContainer { public: //Define the normal iterator types the user will expect typedef list<RadioStation>::iterator iterator; typedef list<RadioStation>::const_iterator const_iterator; //Default constructor and copy constructor RadioStationContainer() {} RadioStationContainer(const RadioStationContainer& rc) { copy(rc.begin(), rc.end(), back_inserter(stations)); } //push_back() defined with the normal container interface void push_back(const RadioStation& s) { stations.push_back(s); } //iterators for working with both const and non const RadioStationContainers iterator begin() { return stations.begin(); } iterator end() { return stations.end(); } const_iterator begin() const { return stations.begin(); } const_iterator end() const { return stations.end(); } private: list<RadioStation> stations; };
This will fit nicely with how a user of the library expects a container to behave. But there is more! This will also fit very nicely with how the Standard Template Library expects a container to behave! You have already seen an example, using copy
and back_inserter
in the copy constructor. But now the user is also free to use transform
, for_each
etc:
void doStuffWithStation(RadioStation& s); void f(RadioStationContainer& rc) { for_each(rc.begin(), rc.end(), doStuffWithStation); }
So when in doubt, always try to fit in.