There are a lot of posts complaining about operator overloading.
I felt I had to clarify the "operator overloading" concepts, offering an alternative viewpoint on this concept.
Code obfuscating?
This argument is a fallacy.
Obfuscating is possible in all languages...
It is as easy to obfuscate code in C or Java through functions/methods as it is in C++ through operator overloads:
// C++
T operator + (const T & a, const T & b) // add ?
{
T c ;
c.value = a.value - b.value ; // subtract !!!
return c ;
}
// Java
static T add (T a, T b) // add ?
{
T c = new T() ;
c.value = a.value - b.value ; // subtract !!!
return c ;
}
/* C */
T add (T a, T b) /* add ? */
{
T c ;
c.value = a.value - b.value ; /* subtract !!! */
return c ;
}
...Even in Java's standard interfaces
For another example, let's see the Cloneable
interface in Java:
You are supposed to clone the object implementing this interface. But you could lie. And create a different object. In fact, this interface is so weak you could return another type of object altogether, just for the fun of it:
class MySincereHandShake implements Cloneable
{
public Object clone()
{
return new MyVengefulKickInYourHead() ;
}
}
As the Cloneable
interface can be abused/obfuscated, should it be banned on the same grounds C++ operator overloading is supposed to be?
We could overload the toString()
method of a MyComplexNumber
class to have it return the stringified hour of the day. Should the toString()
overloading be banned, too? We could sabotage MyComplexNumber.equals
to have it return a random value, modify the operands... etc. etc. etc..
In Java, as in C++, or whatever language, the programmer must respect a minimum of semantics when writing code. This means implementing a add
function that adds, and Cloneable
implementation method that clones, and a ++
operator than increments.
What's obfuscating anyway?
Now that we know that code can be sabotaged even through the pristine Java methods, we can ask ourselves about the real use of operator overloading in C++?
Clear and natural notation: methods vs. operator overloading?
We'll compare below, for different cases, the "same" code in Java and C++, to have an idea of which kind of coding style is clearer.
Natural comparisons:
// C++ comparison for built-ins and user-defined types
bool isEqual = A == B ;
bool isNotEqual = A != B ;
bool isLesser = A < B ;
bool isLesserOrEqual = A <= B ;
// Java comparison for user-defined types
boolean isEqual = A.equals(B) ;
boolean isNotEqual = ! A.equals(B) ;
boolean isLesser = A.comparesTo(B) < 0 ;
boolean isLesserOrEqual = A.comparesTo(B) <= 0 ;
Please note that A and B could be of any type in C++, as long as the operator overloads are provided. In Java, when A and B are not primitives, the code can become very confusing, even for primitive-like objects (BigInteger, etc.)...
Natural array/container accessors and subscripting:
// C++ container accessors, more natural
value = myArray[25] ; // subscript operator
value = myVector[25] ; // subscript operator
value = myString[25] ; // subscript operator
value = myMap["25"] ; // subscript operator
myArray[25] = value ; // subscript operator
myVector[25] = value ; // subscript operator
myString[25] = value ; // subscript operator
myMap["25"] = value ; // subscript operator
// Java container accessors, each one has its special notation
value = myArray[25] ; // subscript operator
value = myVector.get(25) ; // method get
value = myString.charAt(25) ; // method charAt
value = myMap.get("25") ; // method get
myArray[25] = value ; // subscript operator
myVector.set(25, value) ; // method set
myMap.put("25", value) ; // method put
In Java, we see that for each container to do the same thing (access its content through an index or identifier), we have a different way to do it, which is confusing.
In C++, each container uses the same way to access its content, thanks to operator overloading.
Natural advanced types manipulation
The examples below use a Matrix
object, found using the first links found on Google for "Java Matrix object" and "C++ Matrix object":
// C++ YMatrix matrix implementation on CodeProject
// http://www.codeproject.com/KB/architecture/ymatrix.aspx
// A, B, C, D, E, F are Matrix objects;
E = A * (B / 2) ;
E += (A - B) * (C + D) ;
F = E ; // deep copy of the matrix
// Java JAMA matrix implementation (seriously...)
// http://math.nist.gov/javanumerics/jama/doc/
// A, B, C, D, E, F are Matrix objects;
E = A.times(B.times(0.5)) ;
E.plusEquals(A.minus(B).times(C.plus(D))) ;
F = E.copy() ; // deep copy of the matrix
And this is not limited to matrices. The BigInteger
and BigDecimal
classes of Java suffer from the same confusing verbosity, whereas their equivalents in C++ are as clear as built-in types.
Natural iterators:
// C++ Random Access iterators
++it ; // move to the next item
--it ; // move to the previous item
it += 5 ; // move to the next 5th item (random access)
value = *it ; // gets the value of the current item
*it = 3.1415 ; // sets the value 3.1415 to the current item
(*it).foo() ; // call method foo() of the current item
// Java ListIterator<E> "bi-directional" iterators
value = it.next() ; // move to the next item & return the value
value = it.previous() ; // move to the previous item & return the value
it.set(3.1415) ; // sets the value 3.1415 to the current item
Natural functors:
// C++ Functors
myFunctorObject("Hello World", 42) ;
// Java Functors ???
myFunctorObject.execute("Hello World", 42) ;
Text concatenation:
// C++ stream handling (with the << operator)
stringStream << "Hello " << 25 << " World" ;
fileStream << "Hello " << 25 << " World" ;
outputStream << "Hello " << 25 << " World" ;
networkStream << "Hello " << 25 << " World" ;
anythingThatOverloadsShiftOperator << "Hello " << 25 << " World" ;
// Java concatenation
myStringBuffer.append("Hello ").append(25).append(" World") ;
Ok, in Java you can use MyString = "Hello " + 25 + " World" ;
too... But, wait a second: This is operator overloading, isn't it? Isn't it cheating???
:-D
Generic code?
The same generic code modifying operands should be usable both for built-ins/primitives (which have no interfaces in Java), standard objects (which could not have the right interface), and user-defined objects.
For example, calculating the average value of two values of arbitrary types:
// C++ primitive/advanced types
template<typename T>
T getAverage(const T & p_lhs, const T & p_rhs)
{
return (p_lhs + p_rhs) / 2 ;
}
int intValue = getAverage(25, 42) ;
double doubleValue = getAverage(25.25, 42.42) ;
complex complexValue = getAverage(cA, cB) ; // cA, cB are complex
Matrix matrixValue = getAverage(mA, mB) ; // mA, mB are Matrix
// Java primitive/advanced types
// It won't really work in Java, even with generics. Sorry.
Discussing operator overloading
Now that we have seen fair comparisons between C++ code using operator overloading, and the same code in Java, we can now discuss "operator overloading" as a concept.
Operator overloading existed since before computers
Even outside of computer science, there is operator overloading: For example, in mathematics, operators like +
, -
, *
, etc. are overloaded.
Indeed, the signification of +
, -
, *
, etc. changes depending on the types of the operands (numerics, vectors, quantum wave functions, matrices, etc.).
Most of us, as part of our science courses, learned multiple significations for operators, depending on the types of the operands. Did we find them confusing, them?
Operator overloading depends on its operands
This is the most important part of operator overloading: Like in mathematics, or in physics, the operation depends on its operands' types.
So, know the type of the operand, and you will know the effect of the operation.
Even C and Java have (hard-coded) operator overloading
In C, the real behavior of an operator will change according to its operands. For example, adding two integers is different than adding two doubles, or even one integer and one double. There is even the whole pointer arithmetic domain (without casting, you can add to a pointer an integer, but you cannot add two pointers...).
In Java, there is no pointer arithmetic, but someone still found string concatenation without the +
operator would be ridiculous enough to justify an exception in the "operator overloading is evil" creed.
It's just that you, as a C (for historical reasons) or Java (for personal reasons, see below) coder, you can't provide your own.
In C++, operator overloading is not optional...
In C++, operator overloading for built-in types is not possible (and this is a good thing), but user-defined types can have user-defined operator overloads.
As already said earlier, in C++, and to the contrary to Java, user-types are not considered second-class citizens of the language, when compared to built-in types. So, if built-in types have operators, user types should be able to have them, too.
The truth is that, like the toString()
, clone()
, equals()
methods are for Java (i.e. quasi-standard-like), C++ operator overloading is so much part of C++ that it becomes as natural as the original C operators, or the before mentioned Java methods.
Combined with template programming, operator overloading becomes a well known design pattern. In fact, you cannot go very far in STL without using overloaded operators, and overloading operators for your own class.
...but it should not be abused
Operator overloading should strive to respect the semantics of the operator. Do not subtract in a +
operator (as in "do not subtract in a add
function", or "return crap in a clone
method").
Cast overloading can be very dangerous because they can lead to ambiguities. So they should really be reserved for well defined cases. As for &&
and ||
, do not ever overload them unless you really know what you're doing, as you'll lose the the short circuit evaluation that the native operators &&
and ||
enjoy.
So... Ok... Then why it is not possible in Java?
Because James Gosling said so:
I left out operator overloading as a fairly personal choice because I had seen too many people abuse it in C++.
James Gosling. Source: http://www.gotw.ca/publications/c_family_interview.htm
Please compare Gosling's text above with Stroustrup's below:
Many C++ design decisions have their roots in my dislike for forcing people to do things in some particular way [...] Often, I was tempted to outlaw a feature I personally disliked, I refrained from doing so because I did not think I had the right to force my views on others.
Bjarne Stroustrup. Source: The Design and Evolution of C++ (1.3 General Background)
Would operator overloading benefit Java?
Some objects would greatly benefit from operator overloading (concrete or numerical types, like BigDecimal, complex numbers, matrices, containers, iterators, comparators, parsers etc.).
In C++, you can profit from this benefit because of Stroustrup's humility. In Java, you're simply screwed because of Gosling's personal choice.
Could it be added to Java?
The reasons for not adding operator overloading now in Java could be a mix of internal politics, allergy to the feature, distrust of developers (you know, the saboteur ones that seem to haunt Java teams...), compatibility with the previous JVMs, time to write a correct specification, etc..
So don't hold your breath waiting for this feature...
But they do it in C#!!!
Yeah...
While this is far from being the only difference between the two languages, this one never fails to amuse me.
Apparently, the C# folks, with their "every primitive is a struct
, and a struct
derives from Object", got it right at first try.
Despite all the FUD against used defined operator overloading, the following languages support it: Kotlin, Scala, Dart, Python, F#, C#, D, Algol 68, Smalltalk, Groovy, Perl 6, C++, Ruby, Haskell, MATLAB, Eiffel, Lua, Clojure, Fortran 90, Swift, Ada, Delphi 2005...
So many languages, with so many different (and sometimes opposing) philosophies, and yet they all agree on that point.
Food for thought...
Common operators to overload
Most of the work in overloading operators is boiler-plate code. That is little wonder, since operators are merely syntactic sugar, their actual work could be done by (and often is forwarded to) plain functions. But it is important that you get this boiler-plate code right. If you fail, either your operator’s code won’t compile or your users’ code won’t compile or your users’ code will behave surprisingly.
Assignment Operator
There's a lot to be said about assignment. However, most of it has already been said in GMan's famous Copy-And-Swap FAQ, so I'll skip most of it here, only listing the perfect assignment operator for reference:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Bitshift Operators (used for Stream I/O)
The bitshift operators <<
and >>
, although still used in hardware interfacing for the bit-manipulation functions they inherit from C, have become more prevalent as overloaded stream input and output operators in most applications. For guidance overloading as bit-manipulation operators, see the section below on Binary Arithmetic Operators. For implementing your own custom format and parsing logic when your object is used with iostreams, continue.
The stream operators, among the most commonly overloaded operators, are binary infix operators for which the syntax specifies no restriction on whether they should be members or non-members.
Since they change their left argument (they alter the stream’s state), they should, according to the rules of thumb, be implemented as members of their left operand’s type. However, their left operands are streams from the standard library, and while most of the stream output and input operators defined by the standard library are indeed defined as members of the stream classes, when you implement output and input operations for your own types, you cannot change the standard library’s stream types. That’s why you need to implement these operators for your own types as non-member functions.
The canonical forms of the two are these:
std::ostream& operator<<(std::ostream& os, const T& obj)
{
// write obj to stream
return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
// read obj from stream
if( /* no valid object of T found in stream */ )
is.setstate(std::ios::failbit);
return is;
}
When implementing operator>>
, manually setting the stream’s state is only necessary when the reading itself succeeded, but the result is not what would be expected.
Function call operator
The function call operator, used to create function objects, also known as functors, must be defined as a member function, so it always has the implicit this
argument of member functions. Other than this, it can be overloaded to take any number of additional arguments, including zero.
Here's an example of the syntax:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
Usage:
foo f;
int a = f("hello");
Throughout the C++ standard library, function objects are always copied. Your own function objects should therefore be cheap to copy. If a function object absolutely needs to use data which is expensive to copy, it is better to store that data elsewhere and have the function object refer to it.
Comparison operators
The binary infix comparison operators should, according to the rules of thumb, be implemented as non-member functions1. The unary prefix negation !
should (according to the same rules) be implemented as a member function. (but it is usually not a good idea to overload it.)
The standard library’s algorithms (e.g. std::sort()
) and types (e.g. std::map
) will always only expect operator<
to be present. However, the users of your type will expect all the other operators to be present, too, so if you define operator<
, be sure to follow the third fundamental rule of operator overloading and also define all the other boolean comparison operators. The canonical way to implement them is this:
inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}
The important thing to note here is that only two of these operators actually do anything, the others are just forwarding their arguments to either of these two to do the actual work.
The syntax for overloading the remaining binary boolean operators (||
, &&
) follows the rules of the comparison operators. However, it is very unlikely that you would find a reasonable use case for these2.
1 As with all rules of thumb, sometimes there might be reasons to break this one, too. If so, do not forget that the left-hand operand of the binary comparison operators, which for member functions will be *this
, needs to be const
, too. So a comparison operator implemented as a member function would have to have this signature:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Note the const
at the end.)
2 It should be noted that the built-in version of ||
and &&
use shortcut semantics. While the user defined ones (because they are syntactic sugar for method calls) do not use shortcut semantics. User will expect these operators to have shortcut semantics, and their code may depend on it, Therefore it is highly advised NEVER to define them.
Arithmetic Operators
Unary arithmetic operators
The unary increment and decrement operators come in both prefix and postfix flavor. To tell one from the other, the postfix variants take an additional dummy int argument. If you overload increment or decrement, be sure to always implement both prefix and postfix versions.
Here is the canonical implementation of increment, decrement follows the same rules:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Note that the postfix variant is implemented in terms of prefix. Also note that postfix does an extra copy.2
Overloading unary minus and plus is not very common and probably best avoided. If needed, they should probably be overloaded as member functions.
2 Also note that the postfix variant does more work and is therefore less efficient to use than the prefix variant. This is a good reason to generally prefer prefix increment over postfix increment. While compilers can usually optimize away the additional work of postfix increment for built-in types, they might not be able to do the same for user-defined types (which could be something as innocently looking as a list iterator). Once you got used to do i++
, it becomes very hard to remember to do ++i
instead when i
is not of a built-in type (plus you'd have to change code when changing a type), so it is better to make a habit of always using prefix increment, unless postfix is explicitly needed.
Binary arithmetic operators
For the binary arithmetic operators, do not forget to obey the third basic rule operator overloading: If you provide +
, also provide +=
, if you provide -
, do not omit -=
, etc. Andrew Koenig is said to have been the first to observe that the compound assignment operators can be used as a base for their non-compound counterparts. That is, operator +
is implemented in terms of +=
, -
is implemented in terms of -=
etc.
According to our rules of thumb, +
and its companions should be non-members, while their compound assignment counterparts (+=
etc.), changing their left argument, should be a member. Here is the exemplary code for +=
and +
; the other binary arithmetic operators should be implemented in the same way:
class X {
X& operator+=(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator+(X lhs, const X& rhs)
{
lhs += rhs;
return lhs;
}
operator+=
returns its result per reference, while operator+
returns a copy of its result. Of course, returning a reference is usually more efficient than returning a copy, but in the case of operator+
, there is no way around the copying. When you write a + b
, you expect the result to be a new value, which is why operator+
has to return a new value.3
Also note that operator+
takes its left operand by copy rather than by const reference. The reason for this is the same as the reason giving for operator=
taking its argument per copy.
The bit manipulation operators ~
&
|
^
<<
>>
should be implemented in the same way as the arithmetic operators. However, (except for overloading <<
and >>
for output and input) there are very few reasonable use cases for overloading these.
3 Again, the lesson to be taken from this is that a += b
is, in general, more efficient than a + b
and should be preferred if possible.
Array Subscripting
The array subscript operator is a binary operator which must be implemented as a class member. It is used for container-like types that allow access to their data elements by a key.
The canonical form of providing these is this:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
Unless you do not want users of your class to be able to change data elements returned by operator[]
(in which case you can omit the non-const variant), you should always provide both variants of the operator.
If value_type is known to refer to a built-in type, the const variant of the operator should better return a copy instead of a const reference:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Operators for Pointer-like Types
For defining your own iterators or smart pointers, you have to overload the unary prefix dereference operator *
and the binary infix pointer member access operator ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
Note that these, too, will almost always need both a const and a non-const version.
For the ->
operator, if value_type
is of class
(or struct
or union
) type, another operator->()
is called recursively, until an operator->()
returns a value of non-class type.
The unary address-of operator should never be overloaded.
For operator->*()
see this question. It's rarely used and thus rarely ever overloaded. In fact, even iterators do not overload it.
Continue to Conversion Operators
Best Answer
Sets are based on
operator<
(an ordering/equivalence relationship), notoperator==
(which is an equality relationship).To do the thing that you are trying to do, use a custom comparator: