Home > Articles > Programming > C/C++

  • Print
  • + Share This
This chapter is from the book

7.8 Equality and Inequality

C++ teaches its users that any clever trick such as the one presented in the previous section (intentional ambiguity) establishes a new context, which in turn may have unexpected ripples.

Consider tests for equality and inequality of smart pointers. A smart pointer should support the same comparison syntax that raw pointers support. Programmers expect the following tests to compile and run as they do for a raw pointer.




SmartPtr<Something> sp1, sp2;
Something* p;
...
if (sp1)  // Test 1: direct test for non-null pointer
   ...
if (!sp1)    // Test 2: direct test for null pointer
   ...
if (sp1 == 0)   // Test 3: explicit test for null pointer
   ...
if (sp1 == sp2) // Test 4: comparison of two smart pointers
   ...
if (sp1 == p)   // Test 5: comparison with a raw pointer
   ...

There are more tests than depicted here if you consider symmetry and operator!=. If we solve the equality tests, we can easily define the corresponding symmetric and inequality tests.

There is an unfortunate interference between the solution to the previous issue (preventing delete from compiling) and a possible solution to this issue. With one user-defined conversion to the pointee type, most of the test expressions (except test 4) compile successfully and run as expected. The downside is that you can accidentally call the delete operator against the smart pointer. With two user-defined conversions (intentional ambiguity), you detect wrongful delete calls, but none of these tests compiles anymore—they have become ambiguous too.

An additional user-defined conversion to bool helps, but this, to nobody's surprise, introduces new trouble. Given this smart pointer:

template <class T>
class SmartPtr
{
public:
   operator bool() const
   {
      return pointee_ != 0;
   }
   ...
};

the four tests compile, but so do the following nonsensical operations:

SmartPtr<Apple> sp1;
SmartPtr<Orange> sp2; // Orange is unrelated to Apple
if (sp1 == sp2)       // Converts both pointers to bool
                      // and compares results
   ...
if (sp1 != sp2)       // Ditto
   ...
bool b = sp1;         // The conversion allows this, too
if (sp1 * 5 == 200)   // Ouch! SmartPtr behaves like an integral
                      // type!
   ...

As you can see, it's either not at all or too much: Once you add a user-defined conversion to bool, you allow SmartPtr to act as a bool in many more situations than you actually wanted. For all practical purposes, defining an operator bool for a smart pointer is not a smart solution.

A true, complete, rock-solid solution to this dilemma is to go all the way and overload each and every operator separately. This way any operation that makes sense for the bare pointer makes sense for the smart pointer, and nothing else. Here is the code that implements this idea.

template <class T>
class SmartPtr
{
public:
   bool operator!() const // Enables "if (!sp) ..."
   {
      return pointee_ == 0;
   }
   inline friend bool operator==(const SmartPtr& lhs,
      const T* rhs)
   {
      return lhs.pointee_ == rhs;
   }
   inline friend bool operator==(const T* lhs,
      const SmartPtr& rhs)
   {
      return lhs == rhs.pointee_;
   }
   inline friend bool operator!=(const SmartPtr& lhs,
      const T* rhs)
   {
      return lhs.pointee_ != rhs;
   }
   inline friend bool operator!=(const T* lhs,
      const SmartPtr& rhs)
   {
      return lhs != rhs.pointee_;
   }
   ...
};

Yes, it's a pain, but this approach solves the problems with almost all comparisons, including the tests against the literal zero. What the forwarding operators in this code do is to pass operators that client code applies to the smart pointer on to the raw pointer that the smart pointer wraps. No simulation can be more realistic than that.

We still haven't solved the problem completely. If you provide an automatic conversion to the pointee type, there still is the risk of ambiguities. Suppose you have a class Base and a class Derived that inherits Base. Then the following code makes practical sense yet is ill formed due to ambiguity.

SmartPtr<Base> sp;
Derived* p;
...
if (sp == p) {}  // error! Ambiguity between:
                 // '(Base*)sp == (Base*)p'
                 // and 'operator==(sp, (Base*)p)'

Indeed, smart pointer development is not for the faint of heart.

We're not out of bullets, though. In addition to the definitions of operator== and operator!=, we can add templated versions of them, as you can see in the following code:

template <class T>
class SmartPtr
{
public:
   ... as above ...
   template <class U>
   inline friend bool operator==(const SmartPtr& lhs,
      const U* rhs)
   {
      return lhs.pointee_ == rhs;
   }
   template <class U>
   inline friend bool operator==(const U* lhs,
      const SmartPtr& rhs)
   {
      return lhs == rhs.pointee_;
   }
   ... similarly defined operator!= ...
};

The templated operators are “greedy” in the sense that they match comparisons with any pointer type whatsoever, thus consuming the ambiguity.

If that's the case, why should we keep the nontemplated operators—the ones that take the pointee type? They never get a chance to match, because the template matches any pointer type, including the pointee type itself.

The rule that “never” actually means “almost never” applies here, too. In the test if (sp == 0), the compiler tries the following matches.

  • The templated operators. They don't match because zero is not a pointer type. A literal zero can be implicitly converted to a pointer type, but template matching does not include conversions.

  • The nontemplated operators. After eliminating the templated operators, the compiler tries the nontemplated ones. One of these operators kicks in through an implicit conversion from the literal zero to the pointee type. Had the nontemplated operators not existed, the test would have been an error.

In conclusion, we need both the nontemplated and the templated comparison operators.

Let's see now what happens if we compare two SmartPtrs instantiated with different types.

SmartPtr<Apple> sp1;
SmartPtr<Orange> sp2;
if (sp1 == sp2)
   ...

The compiler chokes on the comparison because of an ambiguity: Each of the two SmartPtr instantiations defines an operator==, and the compiler does not know which one to choose. We can dodge this problem by defining an “ambiguity buster” as shown:

template <class T>
class SmartPtr
{
public:
   // Ambiguity buster
   template <class U>
   bool operator==(const SmartPtr<U>& rhs) const
   {
      return pointee_ == rhs.pointee_;
   }
   // Similarly for operator!=
   ...
};

This newly added operator is a member that specializes exclusively in comparing SmartPtr<...> objects. The beauty of this ambiguity buster is that it makes smart pointer comparisons act like raw pointer comparisons. If you compare two smart pointers to Apple and Orange, the code will be essentially equivalent to comparing two raw pointers to Apple and Orange. If the comparison makes sense, then the code compiles; otherwise, it's a compile-time error.

SmartPtr<Apple> sp1;
SmartPtr<Orange> sp2;
if (sp1 == sp2)   // Semantically equivalent to
                  // sp1.pointee_ == sp2.pointee_
   ...

There is one unsatisfied syntactic artifact left, namely, the direct test if (sp). Here life becomes really interesting. The if statement applies only to expressions of arithmetic and pointer type. Consequently, to allow if (sp) to compile, we must define an automatic conversion to either an arithmetic or a pointer type.

A conversion to arithmetic type is not recommended, as the earlier experience with operator bool witnesses. A pointer is not an arithmetic type, period. A conversion to a pointer type makes a lot more sense, and here the problem branches.

If you want to provide automatic conversions to the pointee type (see previous section), then you have two choices: Either you risk unattended calls to operator delete, or you forgo the if (sp) test. The tiebreaker is between the lack of convenience and a risky life. The winner is safety, so you cannot write if (sp). Instead, you can choose between if(sp != 0) and the more baroque if (!!sp). End of story.

If you don't want to provide automatic conversions to the pointee type, there is an interesting trick you can use to make if (sp) possible. Inside the SmartPtr class template, define an inner class Tester and define a conversion to Tester*, as shown in the following code:

template <class T>
class SmartPtr
{
   class Tester
   {
      void operator delete(void*);
   };
public:
   operator Tester*() const
   {
      if (!pointee_) return 0;
      static Tester test;
      return &test;
   }
   ...
};

Now if you write if (sp), operator Tester* enters into action. This operator returns a null value if and only if pointee_ is null. Tester itself disables operator delete, so if somebody calls delete sp, a compile-time error occurs. Interestingly, Tester's definition itself lies in the private part of SmartPtr, so the client code cannot do anything else with it.

SmartPtr addresses the issue of tests for equality and inequality as follows:

  • Define operator== and operator!= in two flavors (templated and nontemplated).

  • Define operator!.

  • If you allow automatic conversion to the pointee type, then define an additional conversion to void* to ambiguate a call to the delete operator intentionally; otherwise, define a private inner class Tester that declares a private operator delete, and define a conversion to Tester* for SmartPtr that returns a null pointer if and only if the pointee object is null.

  • + Share This
  • 🔖 Save To Your Account