Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Overriding the C++ Operator==

By Daniel E. Stevenson and Andrew T. Phillips

, August 01, 2003


Aug03: Overriding the C++ Operator==

Daniel is an assistant professor and Andrew chair of the Computer Science Department at the University of Wisconsin-Eau Claire. They can be contacted at [email protected] and [email protected], respectively.


In his article "How Do I Correctly Implement the equals() Method?" (DDJ, May 2002), Tal Cohen presented a software design for overriding the Java equals method, a technique that is both elegant in design and (more importantly) semantically correct. In this article, we extend Tal's ideas in two ways. First, we present a design that handles complex inheritance situations, including cases where you want to have groups of different objects (from different classes) that remain members of the same equivalence class. Second, we present our design in C++, as well as in Java, to show that the basic design isn't a good Java design as much as it is a good object-oriented design.

Before jumping into the C++ version, we briefly review Tal's original design, but with two variations: Listing One is our version of this Java design (see our paper "Implementing Object Equivalence in Java Using the Template Method Design Pattern," Proceedings of the 34th SIGCSE Technical Symposium on Computer Science Education, 2002). In our design, we have made explicit use of the Template Method design pattern (see Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma et al., Addison-Wesley, 1995) to factor out each class-specific definition of Tal's blindlyEquals so that our implementation of equals is now completely independent of any specific class. Thus, we have moved equals into the abstract super class T, the template class, which now supplies the invariant steps required to correctly implement equality. The variant code is still supplied by each subclass, such as our class A with its single private data member x, in blindlyEquals.

Our second variation from the original design is in our inclusion of the method getTypeEquiv, which simply reports the name of the class (type) to which that instance wishes to belong. Following this design, instances from two different classes will now be able to report that they are really to be treated as the same type. This is important because the only way for two instances to be equal is if their corresponding types are the same and they both inherit from the super class T. Hence, each subclass will necessarily be required to supply a unique (and protected) version of blindlyEquals and of getTypeEquiv to make the proper private data-member comparisons and to report the appropriate class type, respectively.

If we now extend the class A with a subclass B containing a single private data member y, Listing Two provides the versions of blindlyEquals and getTypeEquiv for class B, assuming that we do not want A and B to be of the same type.

In our Template Method design, each class-specific implementation of blindlyEquals requires a specific downcast of the argument into the appropriate type; and this cast is typesafe because the actual types of both this and that are checked and guaranteed to be the same by the use of Class.forName in the code for getTypeEquiv. Obviously, if this and that are members of the same class, then they are of identical types; and herein lies a key to why our design correctly adheres to the general contract for equals—namely, that it satisfy the requirements of an equivalence relation.

What if you wanted to define a set of two or more different classes, all of which should be treated as the same type? For example, you might want to extend class A with class B in such a way as to permit overriding of some of the methods of A, but still maintain identical private data members. Or more generally, it might be desirable to simply define two different classes A and B both of which inherit from a common ancestor C, neither of which inherits from the other, and yet consider A and B to be equivalent types for the purposes of comparison. We call this concept "type equivalence." In either case, our design permits these generalizations as long as you adhere to the following rules:

  • All of the classes that are in the same type-equivalent set E must have a common ancestor RE in the inheritance hierarchy, and RE also must be in the set E. Hence, RE is the canonical member for all of the type-equivalent classes in E.
  • The template class T must be a proper ancestor of RE in the inheritance hierarchy.

  • RE must be the only member of the type-equivalent set E to define getTypeEquiv and blindlyEquals. All other members of E will therefore inherit these methods from RE.

  • If A and B are both members of the type-equivalent set E, then all classes between A and B in the inheritance hierarchy must also be in E.

To illustrate how these rules permit a variety of designs of type equivalence while maintaining semantic correctness, encapsulation, and code reuse, consider the example inheritance hierarchy in Figure 1.

Figure 1 shows three distinct type-equivalent sets: A=A1,A2}, B={B1,B2,B3,B4}, and C={C1}. Each type-equivalent set has a top-most member (the canonical member RE) of the inheritance hierarchy: These are A1 for A, B1 for B, and C1 for C in this example. Each of these canonical members is therefore required to define a version of blindlyEquals and of getTypeEquiv, but none of the other members of the inheritance hierarchy in this example would contain such methods. In particular, the subclasses B2, B3, and B4 all would inherit blindlyEquals and getTypeEquiv from B1 because they are all in the same type-equivalent set as B1.

Using this design allows two instances of the type-equivalent set B to be tested for equality based solely on the private data members of B1. The fact that the two distinct instances may not be of the same actual Java type is of no consequence here because the downcast from an Object to a B1 performed in blindlyEquals is still typesafe (as guaranteed by getTypeEquiv). And, of course, the semantic correctness of equals is still maintained by our Template Method design.

Now for our C++ design, since one test of the value and generality of a good design is whether the same design is cleanly transferable to a different object-oriented language. In the case of our equals design and C++, we are interested in the overloading of the operator==.

Many of the same design and implementation issues relevant in Java are also issues in C++—namely, providing the proper signatures, testing the argument for Null, ensuring this and that are type equivalent and then performing the corresponding downcast, and most importantly, ensuring that the operation actually implements an equivalence relation. In each case, there is a simple, if not particularly elegant, C++ solution. As an example, Listing Three shows the C++ versions of the template base class T and subclass A, now implemented in C++.

The design similarities between the C++ version and our Java version are intentional and apparent. The only significant change in the design of the code (ignoring the obvious syntax and implementation-specific details of typecasting) is that the C++ version of operator== requires the template type T to be the argument in the signature because C++ has no overarching Object. This also permits us to drop the explicit that instanceof T inheritance check because this type compatibility test is required for compilation anyway. Also, the method blindlyEquals must explicitly be declared virtual (which is automatic in Java) so that all subclasses can override it and have their blindlyEquals methods selected dynamically (but the operator== method should not be virtual so as to discourage that dynamic behavior). Aside from this, the generality of our design makes it easily transferable to C++.

Regarding the importance of providing the proper method signatures for the operator==, consider the following common and yet flawed design, illustrated using UML in Figure 2, for overloading the operator== in a simple two-class inheritance hierarchy.

If you construct a signature tailored to the specific class (as is commonly done), such an approach will surely lead to a serious flaw. Consider Listing Four. In Figure 2, the operator== is overloaded in class B, and the argument for that method can accept an object of type B but not A. What happens in Listing Four? Java developers would answer that because class A has a version of equals with a signature that can accept an A, the result would be a call to the operator== method available in class A. But in C++, the result is actually quite surprising—the code doesn't compile. Why? Because the C++ mechanism for resolving dynamic method invocation uses a two-step process that:

1. Finds the first enclosing scope in which the method name is defined (in this case, class B).

2. Tries to match the signature with a method in that scope only.

In our example, there is no method in class B with a signature for operator== that can accept an argument of type A, even using a legal cast. Hence, the code won't compile. Of course, if the order of instanceB and instanceA were reversed in the conditional test of the example in Listing Four (that is, *instanceA == *instanceB), the example would not only compile, but the code in class A would be executed. Thus, the operator== must be overloaded using only the most general base class (which is T).

If you take the position that each distinct class should define its own distinct type-equivalent set (and we certainly do), then the design is greatly simplified. In this case, getTypeEquiv would be moved into the template class T and made protected and final (or nonvirtual in C++) as in Listing Five. In this way, each class identifies itself (using getClass in Java or typeid in C++) as distinct from all other classes, and there is no need or way for any class to ever override getTypeEquiv.

DDJ

Listing One

abstract class T {
    public final boolean equals(Object that) {
        boolean isEqual = false;
        if ((that != null) && (that instanceof T)) {
            T castedThat = (T) that;
            if (this.getTypeEquiv().equals(castedThat.getTypeEquiv())) {
                isEqual = blindlyEquals(that);
            }
        }
        return isEqual;
    }
    protected boolean blindlyEquals(Object that) {
        return true; // to stop the chaining
    }
    abstract protected Class getTypeEquiv();
}
class A extends T {
    protected boolean blindlyEquals(Object that) {
        A castedThat = (A) that;
        // perform comparisons on private data
        boolean isEqual = (this.x == castedThat.x);
        return (isEqual && super.blindlyEquals(that));
    }
   protected Class getTypeEquiv() {
        Class result = null;
        try { // will never fail, but must try/catch
            result = Class.forName("A");
        } catch (ClassNotFoundExeception e) { }
        return result;
    }
    private int x;
}

Back to Article

Listing Two

class B extends A {
    protected boolean blindlyEquals(Object that) {
        B castedThat = (B) that;
        // perform comparisons on private data
        boolean isEqual = (this.y == castedThat.y);
        return (isEqual && super.blindlyEquals(that));
    }
    protected Class getTypeEquiv() {
        Class result = null;
        try { // will never fail, but must try/catch
            result = Class.forName("B");
        } catch (ClassNotFoundExeception e) { }
        return result;
    }
    private int y;
}

Back to Article

Listing Three

class T {
public:
    bool operator==(const T &that) const {
        bool isEqual = false;
        if ((&that != NULL) && (*(this->getTypeEquiv()) == *(that.getTypeEquiv()))) {
            isEqual = (this->blindlyEquals(&that));
        }
        return isEqual;
    }
protected:
    virtual bool blindlyEquals(const T *that) const {
        return true; // default to stop the chaining
    }
    virtual const type_info* getTypeEquiv() const = 0;
};
class A : public T {
protected:
    virtual bool blindlyEquals(const T *that) const {
        const A *castedThat = dynamic_cast<const A*>(that);
        // perform comparisons on private data
        bool isEqual = (this->x == castedThat->x);
        return (isEqual && T::blindlyEquals(that));
    }
    virtual const type_info* getTypeEquiv() const {
        return &typeid(*(new A()));
    }
private:
    int x;
};

Back to Article

Listing Four

A *instanceA = new A;
B *instanceB = new B;
if (*instanceB == *instanceA) {
    // which version of == will run??
}

Back to Article

Listing Five

abstract class T { // Java version
 ... // same as before
    protected final Class getTypeEquiv() {
        return this.getClass();
    }
}
class T { // C++ version

protected:
    const type_info* getTypeEquiv() const {
        return &typeid(*this);
    }
}

Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.