Gotcha #23: Operator Function Lookup Anomaly
Overloaded operators are really just standard member or non-member functions that may be invoked using infix syntax. They're syntactic sugar:
class String {
public:
String &operator =( const String & );
friend String operator +( const String &, const String & );
String operator –();
operator const char *() const;
// . . .
};
String a, b, c;
// . . .
a = b;
a.operator =( b ); // same
a + b;
operator +( a, b ); // same
a = -b;
a.operator =( b.operator –() ); // same
const char *cp = a;
cp = a.operator const char *(); // same
I think we can make a case for superior clarity in the case of the infix notation. Typically, we would employ infix notation when using an overloaded operator; after all, that's why we overloaded the operator in the first place.
Common exceptions to the use of infix notation would be when the function call syntax is clearer than the corresponding infix call. One standard example is the invocation of a base class's copy assignment operator from the implementation of the derived class copy assignment operator:
class A {
protected:
A &operator =( const A & );
// . . .
};
class B : public A {
public:
B &operator =( const B & );
// . . .
};
B &B::operator =( const B &b ) {
if( &b != this ) {
A::operator =( b ); // clearer than
// (*static_cast<A*const>(this))=b
// assign local members . . .
}
return *this;
}
The function call form is also used in preference to infix when the infix usage—though perfectly correct—is so weird that it would cost a reader a couple of minutes to figure it out:
value_type *Iter::operator ->() const
{ return &operator *(); } // rather than &*(*this)
There are also ambiguous cases, in which neither the infix nor non-infix syntax offers a clear advantage :
bool operator !=( const Iter &that ) const
{ return !(*this == that); } // or !operator ==(that)
However, note that the lookup sequence for the infix syntax differs from that of the function call syntax. This can produce unexpected results:
class X {
public:
X &operator %( const X & ) const;
void f();
// . . .
};
X &operator %( const X &, int );
void X::f() {
X &anX = *this;
anX % 12; // OK, non-member
operator %( anX, 12 ); // error!
}
The use of the function call syntax follows the standard lookup sequence in searching for the function name. In the case of the member function X::f, the compiler will first look in the class X for a function named operator %. Once it finds the name, it won't continue looking in outer scopes for additional functions named operator %.
Unfortunately, we're attempting to pass three arguments to a binary operator. Because the member function operator % has an implicit this argument, the two explicit arguments imply to the compiler that we're attempting to make binary % a ternary operator. A correct call would either identify the nonmember function explicitly (::operator %( anX, 12 )) or pass the correct number of arguments to the member function (operator %( anX ) ).
Using the infix notation causes the compiler to search in the scope indicated by the left operand (that is, in class X, since anX is of type X) for a member operator % and to search for a non-member operator %. In the case of the expression anX % 12, the compiler will identify two candidate functions and correctly match on the non-member function.
|