The polymorphic equality problem Thursday 21st February 2008
For many situations the use of RTTI in C++ is unnecessary and it can often be an indicator of poor design. There are, however, some circumstances in which a dynamic_cast can make things cleaner and reduce dependencies.
For many classes an equality operator is desirable, its implementation often passes through to its members in a natural way, however some classes may hold pointers (or auto_ptr) to base types because they require a member to have polymorphic behaviour but that member should still have value semantics.
The problem then, is how to compare two objects through pointers to their common base class. The result of the comparison should be result of comparing the derived objects if the derived types are the same and false otherwise.
Here is my “dynamic_cast” solution.
class Base { public: virtual ~Base() {} virtual bool equal( const Base& b ) const = 0; bool operator==( const Base& b ) const { return equal( b ); } }; class Derived1 : public Base { public: Derived1( int v ) : _val( v ) {} bool equal( const Base& b ) const { const Derived1* pb = dynamic_cast< const Derived1* >( &b ); return pb != 0 && *this == *pb; } bool operator==( const Derived1& other ) const { return _val == other._val; } private: int _val; }; class Derived2: public Base { public: Derived2( int v ) : _val( v ) {} bool equal( const Base& b ) const { const Derived2* pb = dynamic_cast< const Derived2* >( &b ); return pb != 0 && *this == *pb; } bool operator==( const Derived2& other ) const { return _val == other._val; } private: int _val; };
Of course this part is just repetitive boiler plate code so we should split it out into a function.
bool equal( const Base& b ) const { const Derived1* pb = dynamic_cast< const Derived1* >( &b ); return pb != 0 && *this == *pb; }
Of course, it relies on a type for the correct function so it needs to be a function template.
template< class BaseT, class DerivedT > bool IsEqual( const DerivedT* d, const BaseT& b ) { const DerivedT* pb = dynamic_cast< const DerivedT* >( &b ); return pb != 0 && *this == *pb; } bool Derived1::equal( const Base& b ) const { return IsEqual( this, b ); }
Note how we can use template argument deduction so that we don’t have to explicitly specify ‘Derived1’. If you fancy, you can use this to make a macro and just put the macro in each class declaration. This is one step beyond what I’d do, though.
#define DOES_BASE_POLY_EQ bool equal( const Base&b ) const { return IsEqual( this, b ); }
Of course, always using a == b might not be correct for all classes. This behaviour could be made parameterizable.
template< class T, class U, class E = std::equal_to< T > > struct IsEqualHelper { bool operator()( const T* t, const U& u ) const { const T* ut = dynamic_cast< const T* >( &u ); return ut != 0 && E()( *t, *ut ); } }; template< class T, class U > bool IsEqual( const T* t, const U& u ) { return IsEqualHelper< T, U >()( t, u ); }
We retain the IsEqual template function for ease of use in the simple case, but provide a template struct with an operator() and a parameterizable equality functor for which there is an obvious default in the standard C++ library.
What’s the alternative without RTTI? Unfortunately it’s a classic double dispatch problem for which there are no universally neat solutions. Here’s what I came up with. It’s not nice.
The basic strategy is that the comparing to base classes calls the virtual equal function on one of them, with the other as parameter. The virtual function mechanism now ensures that we can use a this pointer with the correct derived type in the derived equal function, the other parameter still has a type of base reference, though. To promote this to derived type we call a virtual helper function (iseq) on the base reference which is overloaded for all the different derived types. This ensures that we can now promote the other parameter to a derived pointer through a virtual function without losing information about the type of the first reference. When the two derived types differ, the base implementation (return false) is the correct behaviour. Only the iseq overload for the matching derived class needs to be overridden in each derived class. It can then call the appropriate equality operator for two derived instances.
The big ugliness is the requirement for a virtual function in the base class for each derived type which is a nasty reversed coupling between base and derived classes.
// Forward declarations of every single derived class class Derived1; class Derived2; // ... class Base { public: virtual ~Base() {} virtual bool equal( const Base& b ) const = 0; bool operator==( const Base& b ) const { return equal( b ); } // Default iseq helper virtual function for every single derived class virtual bool iseq( const Derived1& ) const { return false; } virtual bool iseq( const Derived2& ) const { return false; } // ... }; class Derived1 : public Base { public: Derived1( int v ) : _val( v ) {} bool equal( const Base& b ) const { return b.iseq( *this ); } bool operator==( const Derived1& other ) const { return _val == other._val; } virtual bool iseq( const Derived1& other ) const { return *this == other; } private: int _val; }; class Derived2: public Base { public: Derived2( int v ) : _val( v ) {} bool equal( const Base& b ) const { return b.iseq( *this ); } bool operator==( const Derived2& other ) const { return _val == other._val; } virtual bool iseq( const Derived2& other ) const { return *this == other; } private: int _val; };
// Forward declarations of every single derived class
class Derived1;
class Derived2;
I feel sick.
What kind of overhead do you expect in the normal case for switching on rtti? My question amounts to: “is this a religious issue?”.
Of course, almost every issue is religious, so I don’t expect an objective answer.