Introduction
Operator overloading is a powerful and underused (but often misused) feature that can help make your code much simpler and your objects more intuitive to use. Adding some simple operator overloads to your class or struct enables you to:
- allow conversion to and from your type and other types
- perform mathematical/logical operations on your type and itself or other types
Conversion Operators
There are several ways of converting between types, but the most common are using implicit/explicit conversion operators.
Implicit
Implicit means you can assign an instance of one type directly to another.
Let's create an example.
public struct MyIntStruct
{
int m_IntValue;
private MyIntStruct(Int32 intValue)
{
m_IntValue = intValue;
}
public static implicit operator MyIntStruct(Int32 intValue)
{
return new MyIntStruct(intValue);
}
}
Once we have this in our struct, we can create a new instance of it from our code by simply assigning it a value - no casting or new declarations are required in our code.
MyIntStruct myIntStructInstance = 5;
To implicitly convert our struct back to an int
, we need to add another conversion operator:
public static implicit operator Int32(MyIntStruct instance)
{
return instance.m_IntValue;
}
Now, we can assign our myIntStructInstance
directly to an int
.
int i = myIntStructInstance;
We can also call any other function that will take an int
and pass our instance directly to it. We can do this with as many types as we like.
Maybe, our struct also has a string member variable, in which case, it could be useful to be able to create an instance of it by directly assigning from a string. This struct may look like this...
public struct MyIntStringStruct
{
int m_IntValue;
string m_StringValue;
private MyIntStringStruct(Int32 intValue)
{
m_IntValue = intValue;
m_StringValue = string.Empty;
}
private MyIntStringStruct(string stringValue)
{
m_IntValue = 0;
m_StringValue = stringValue;
}
public static implicit operator MyIntStringStruct(Int32 intValue)
{
return new MyIntStringStruct(intValue);
}
public static implicit operator MyIntStringStruct(string stringValue)
{
return new MyIntStringStruct(stringValue);
}
public static implicit operator Int32(MyIntStringStruct instance)
{
return instance.m_IntValue;
}
}
... and we can create an instance by assigning an int
value as before, or a string
.
MyIntStringStruct myIntStringStructInstance = "Hello World";
Notice, I haven't created an implicit conversion operator from our struct to a string
. Doing so can, in some situations, create ambiguity that the compiler cannot deal with. To see this, add the following:
public static implicit operator string(MyIntStringStruct instance)
{
return instance.m_StringValue;
}
This compiles without issue. Now, try this:
MyIntStringStruct myIntStringStructInstance = "Hello World";
Console.WriteLine(myIntStringStructInstance);
The compiler will fail with "The call is ambiguous between the following methods or properties...". The console will happily take an int
or a string
(and many other types, of course), so how do we expect it to know which to use? The answer is to use an explicit conversion operator.
Explicit
Explicit means we have to explicitly perform the cast in our code. Change the keyword implicit
to explicit
in the last conversion operator so it looks like this.
public static explicit operator string(MyIntStringStruct instance)
{
return instance.m_StringValue;
}
Now, we can return the string value, but only if we explicitly cast to string
.
MyIntStringStruct myIntStringStructInstance = "Hello World";
Console.WriteLine((string)myIntStringStructInstance);
We get the output we expected.
Implicit vs. Explicit
Implicit is far more convenient than explicit as we don't have to explicitly cast in our code, but as we have seen above, there can be issues. There are other things to bear in mind, however, such as: if the objects being passed in either direction will be rounded or truncated in anyway, then you should use explicit conversion, so the person using your object isn't caught out when data is discarded.
Imagine a series of complex calculations being done with decimals, and we have allowed our struct to implicitly be converted to decimal for convenience, but the member variable where it is stored is an integer. The user would rightly expect any object that implicitly passes in or out a decimal to retain the precision, but our struct would lose all decimal places, and could cause the end result of their calculations to be significantly wrong!
If they have to explicitly cast to or from our struct, then we are effectively posting a warning sign. In this particular situation, it may be (is) better to inconvenience the user and have neither implicit nor explicit, so they have to cast to an int
to use our struct, and then there is no misunderstanding.
There are others issues too - do some research before using implicit
. If in doubt, use explicit
, or don't implement the conversion operator for that type at all.
Binary operators
Binary operators (surprisingly!) take two arguments. The following operators can be overloaded: +
, -
, *
, /
, %
, &
, |
, ^
, <<
, >>
.
Note: It's important that you don't do anything unexpected when using operators, binary or otherwise.
These operators are normally pretty logical. Let's take the first, +
. This normally adds two arguments together.
int a = 1;
int b = 2;
int c = a + b;
The string
class, however, uses the +
operator for concatenation.
string x = "Hello";
string y = " World";
string z = x + y;
So, depending on your situation, you may do whatever is logically required.
Imagine a struct that holds two int
s, and you have two instances that you wish to add. The logical thing to do would be to add the corresponding int
s in each instance. You may also want to add just one int
to both values. The resulting struct would look something like this.
public struct MySize
{
int m_Width;
int m_Height;
public MySize(int width, int height)
{
m_Width = width;
m_Height = height;
}
public static MySize operator +(MySize mySizeA, MySize mySizeB)
{
return new MySize(
mySizeA.m_Width + mySizeB.m_Width,
mySizeA.m_Height + mySizeB.m_Height);
}
public static MySize operator +(MySize mySize, Int32 value)
{
return new MySize(
mySize.m_Width + value,
mySize.m_Height + value);
}
}
It can then, obviously, be used like this:
MySize a = new MySize(1, 2);
MySize b = new MySize(3, 4);
MySize c = a + b;
MySize d = c + 5;
You can do similar things for all the overloadable binary operators.
Unary operators
Unary operators take just one argument. The following operators can be overloaded: +
, -
, !
, ~
, ++
, --
, true
, false
. Most types have no need to implement all these operators, so only implement the ones you need!
A simple example using ++
on the struct used above:
public static MySize operator ++(MySize mySize)
{
mySize.m_Width++;
mySize.m_Height++;
return mySize;
}
Note: The ++
and --
unary operators should (for standard operations) alter the integral values of your struct, and return the same instance, and not a new instance, with the new values.
Comparison operators
Comparison operators take two arguments. The following operators can be overloaded. [==
, !=
], [<
, >
], [<=
, >=
]. I have grouped these in square brackets as they must be implemented in pairs.
If using ==
and !=
, you should also override Equals(object o)
and GetHashCode()
.
public static bool operator ==(MySize mySizeA, MySize mySizeB)
{
return (mySizeA.m_Width == mySizeB.m_Width &&
mySizeA.m_Height == mySizeB.m_Height);
}
public static bool operator !=(MySize mySizeA, MySize mySizeB)
{
return !(mySizeA == mySizeB);
}
public override bool Equals(object obj)
{
if (obj is MySize)
return this == (MySize)obj;
return false;
}
public override int GetHashCode()
{
return (m_Width * m_Height).GetHashCode();
}
The comparison operators normally return a bool
, although they don't have to, but remember, don't surprise the end user!
Other operators
This just leaves the conditional operators, &&
, ||
, and the assignment operators, +=
, -=
, *=
, /=
, %=
, &=
, |=
, ^=
, <<=
, >>=
. These are not overloadable, but are evaluated using the binary operators. In other words, supply the binary operators, and you get these for free!
Conclusion
I hope this introduction to operator overloading has been useful and informative. If you haven't used them before, give them a try - they can be a useful tool in your box.
- Don't overuse them. If an operator or conversion will definitely never be needed, or it's not obvious what the result will be, don't supply it.
- Don't misuse them. Yes, you can make
++
decrement values, but you most definitely shouldn't!
- Be very careful when implicitly converting your classes or structs to other types.
- Remember, structs are value types, and classes are reference types. It can make a huge difference in how you handle things in your overload methods.
MSDN references
History
- 31 August 2008: Initial version.
- 6 September 2008: Added the
==
, !=
, Equals
, and GetHashCode
examples.
- 2 October 2008: Added note about returning from the
++
and --
overloads.