Sometime ago there was an article in C++ Report on the subject of writing your own operator=(). Despite the considerable efforts of the author there were still faults in his proposals. The reason that I mention that is that I expect there to be some flaws in this article as well. Please find them and send them in. That way we will all be wiser. Indeed I will be most disappointed if none of you have anything to add to this article. The biggest reward I get for writing for publication is the amount I learn from my mistakes.
Obviously I like to be right and it gives me a warm feeling when others adopt some idea of mine (particularly if they are more expert than I am) but the profit comes from learning something new.
The first decision that any class designer has to make with regard to operator=() is what semantics are appropriate to the type in question. The following possibilities spring to mind:
-
Unique Objects - a bit like Java's objects
-
Pure value types - no inheritance anticipated.
-
Polymorphic types
-
With potential ordering across subtypes
-
Without ordering between subtypes.
Let me tackle these in ascending order of difficulty.
This one is very easy. Check the following code:
class Unique { public: bool operator==(Unique const volatile &) const volatile throw(); // rest of interfaces }; inline bool Unique::operator == (Unique const volatile & rhs) const volatile throw() { return (this == &rhs) ? true : false; }
The idea here is that the meaning of identity is that they are the same object. At first sight you may wonder if such a strong definition of identity has any utility. If there were no possibility of aliasing (referencing) the answer would be 'no' but with objects being passed around by reference there may be times when you want to determine if two references to instances of a Unique actually refer to the same instance.
As I was writing this an errant thought crossed my mind, objects that possess this characteristic (identity means the same object) are inherently unordered when it comes to various Standard Library features that handle collections of objects. Yet, some STL features require an ordering. I wonder if this would be good enough?
inline int Unique::compare (Unique const volatile & rhs) const volatile throw() { return (*this == rhs) ? 0 : 1; } inline bool Unique::operator < (Unique const volatile & rhs) const volatile throw( { return compare(rhs) ? true : false; }
I think that needs rather more thought than I currently have time for (the copy date for this issue of Overload was seven days ago :-( ) so it is a chance for you to do the thinking and share the results with the rest of us.
The only problem with operator==() in this case is ensuring that the prototype is correct. There is no problem with derived types because two identifiers can only refer to the same address if the objects are the same (and therefore have the same dynamic type as well). This means that there would seem to be no problem with making operator==() a member function. The only caveat is that you might have problems if you elected to provide some other meaning for operator==() when applied to objects of different derived types. However, I would suggest that any design that supported such a bizarre concept would belong in a fantasy nightmare world.
So how about the inclusion of volatile? The simple question is 'Why not?' There can be a hidden cost to gratuitous usage of volatile (it disables many potential optimisations) but in the instances above the objects are never accessed and so making the implementation as broad as possible should not incur a cost.
A similar argument supports the addition of a throw() exception specification. All the functions above are leaf functions or functions forwarding to leaf functions. If an exception gets raised inside this code you have a serious problem.
Again, I think these functions are simple enough to justify inlining, but perhaps you disagree. If so please share your reasons.
This is not quite as simple as it may seem. We have to ask ourselves what it might mean if someone compared an instance of a derived type with an instance of our type. Of course you have not prepared your pure value type for derivation, but that does not mean that someone will not do it one day. What do you want to do in such a case? There seem to be two choices:
-
Compare the base subobject of the instances
-
Check that you are not comparing derived instances
class PureValue { public: int compare(PureValue const &) const; // rest of interfaces }; inline bool operator == (PureValue lhs, PureValue rhs) { return lhs.compare(rhs) ? false : true; }
Note the differences between this and the unique object case. I have removed the use of volatile because I can no longer get that for free (if I need a volatile case I will need to provide that as a separate function. Actually this seems unlikely.)
When deciding on the low-level design of PureValue you will need to decide whether you are going to pass it around by value or by reference. Only the class designer can know which is better suited to the type being implemented. Of course the way C++ works makes this purely a decision for the class designer. You may think that pass by (const) reference will normally be the way to go but I think this is less obvious than sometimes imagined. Even with the const qualification pass by reference inhibits many desirable optimisations and in the case of multi-threaded code pass by reference has some bad implications. Suppose that some other thread has access to that variable (as a class designer you do not know this will not happen) even though you passed by const & the object might be changed by another thread. Much safer to pass by value and write a copy constructor that locks the instance during copying. (This is another entry point for the thoughtful reader to contribute an article).
Once I decide that I will pass the right-hand operand by value, symmetry suggests that I should handle the left-hand operand the same way. That more or less requires a non-member operator==(). However note that there is no cause for using friend. The compare() member function seems a much better way to go. It is useful for other things as well. Exactly how you implement PureValue::compare() will depend on the details of PureValue. However I think the const & parameter is probably correct this time. The other parameter (this) is being passed as a pointer and you have no choice. If you intend calling PureValue::compare() direct from code that needs to be thread safe you will need to take suitable action at the call site.
What about inlining operator==()? Well why do you think I chose not to? And what happened to the exception specifications? I didn't just get lazy. Think about it.
Suppose that we want to prevent comparisons between base instances and derived instances and we want to 'force' derived classes to do their own work. Now we need a utility to detect breaches of this design constraint. Ideally we would like to detect this problem at compile time. To be honest, I cannot think of any way to do that (cue for another contribution☺). However, the following will manage it at runtime:
class NoDerive { public: bool notDerived() const; int compare(NoDerive const &) const; // rest of interfaces }; bool NoDerive::notDerived() const { return typeid(*this) == typeid(NoDerive); } int NoDerive::compare (NoDerive const & rhs) const { if (!(notDerived() && rhs.notDerived())) throw IllegalComparison(); // normal code } bool operator== (NoDerive const & lhs, NoDerive const & rhs) { return lhs.compare(rhs) ? false : true; }
The NoDerive::notDerived() member function relies on being able to determine the dynamic type of the instance. For this reason all parameters must be passed by (const) reference or as pointers. If it matters this means that the normal code in NoDerive::compare() will need to lock the parameters while they are being accessed. It might be worth considering starting the code by copying the parameters to local variables. By the way you should not get into the habit of thinking that multi-threaded code will be run on a single processor. There are many things that would be safe under such a condition that become disastrous when using multiple processors (something that will become increasingly common in future years).
Again we must consider what we want. Think about Chess pieces. These are a fairly good example of a simple polymorphic type (actually we have an interesting complication because pawns can promote, but we will leave that for another article in a different context). Each Chess piece has its own properties. Some of those are fixed by its fundamental type (legal moves, colour etc) other things vary during a game (position, value to the player - captured pieces have no value, rooks on open files have enhanced value - etc.) Because each side has pairs of rooks, knights etc. we can reasonable ask two questions:
-
Are two pieces the same type?
-
Do they belong to the same side?
The second question does not seem to lend itself to the identity operator as well as the first. My sense is that the second question should be relegated to a member function. Or even to comparing the return value of a member function Colour::whatColour().
Let us look at a draft for a base class for a hierarchy of Chess pieces.
class ChessPiece { public: enum Colour {unknown, black, white}; Colour whatColour(); bool sameType(ChessPiece const &)const; int compare(ChessPiece const &)const; virtual ~ChessPiece() = 0; virtual void move(); private: Colour col; Position pos; };
This is far from a complete design, but it will do for a start. Note that functions such as compare must be in the base class, whereas functions such as move() must be implemented in the relevant derived class (Pawn, King etc) Oddly, in this case we probably would not use compare() to provide an implementation of operator ==(). This will meet our needs perfectly well:
bool operator == (ChessPiece const & lhs, ChessPiece const & rhs) { return lhs.sameType(rhs); }
and the implementation of ChessPiece::sameType() could be something such as:
bool ChessPiece::sameType (ChessPiece const & rhs)const { return typeid(*this) == typeid(rhs); }
While Chess pieces are a good simple example of a flat hierarchy many other hierarchies have considerable depth. The first issue we must face is that if we are to avoid comparing things of different derived types we must provide base class functionality. An ABC with only an interface is not good enough. Using ABC's as equivalent to Java interfaces is fine but we can do more with ours and sometimes that works to our advantage.
As I am running a bit short of time I am simply going to offer you the sample code at the end of this article which I wrote for a discussion in comp.lang.c++. Note that the following code is simply to illustrate a principle. But, feel free to rip it to shreds. The unusual returns in compareInstances simply allow some simple test of concept code.
For testing purposes identity has been treated as 'same object'. In real code do something else.
This design detects attempts to compare derived objects for which no class specific comparison has been provided Unfortunately this is at run time. Any ideas as how to move that forward to compile or link time would be gratefully received.
I would also like to hear of any ideas as to how to force all derived classes to implement a particular function. Pure virtuals are not strong enough as the requirement is satisfied as soon as a derived class provides an implementation. What I want is a way to require diagnosis of the use of any derived class that does not provide its own implementation of a pure virtual in the base class. I considered using a virtual base class on the grounds that it is the most derived class that constructs it, but a few moments of thought convinced me that that would not work. Anyone got any ideas?
#include <iostream> #include <typeinfo> //introduce namespace std using std::typeid; using std::cout; class Base { virtual int compareInstances (Base const & rhs)const { if(typeid(Base) != typeid(*this)) return 99; return (&rhs == this)? 0 : 3; } public: bool sametypes(Base const & rhs) const { return (typeid(*this) == typeid(rhs)) ?true :false; } int compare(Base const & rhs) const { return sametypes(rhs) ?compareInstances(rhs) :2; } virtual ~Base(){}; }; class Derived: public Base { int compareInstances (Base const & rhs)const { if(typeid(Derived)!= typeid(*this)) return 99; return (&rhs == this)? 0 : 4; } }; class MoreDerived : public Derived { // class in which no compareInstances // has been provided }; // wrap the compare member function to // create operator == bool operator == (Base const & lhs, Base const & rhs) { int value = lhs.compare(rhs); if (value == 99) throw "Sliced comparison"; return value ? false : true; } // small test harness int main( void ) { try{ Base b1, b2; Derived d1, d2; cout << (b1==b1) << endl; cout << (d1==d1) << endl; cout << (b1==b2) << endl; cout << (d1==d2) << endl; cout << (b1==d2) << endl; cout << (d1==b2) << endl; MoreDerived md1, md2; cout << (md1==md2) << endl; } catch ( char const * message) { cout << message << endl; } return 0; }
And a final thought, I wonder if a protected template member function in the base class might work. No because it falls foul of the same problem, how can we force its call in the most derived class?
Overload Journal #29 - Dec 1998 + Programming Topics
Browse in : |
All
> Journals
> Overload
> 29
(12)
All > Topics > Programming (877) Any of these categories - All of these categories |