User defined compound operators in C# 14
Operator overloading has been a part of C# since its very first version. The functionality has remained the same until the introduction of generic math in C# 11 when some changes were made to shift operator overloading and support for checked overloaded operators was added. C# 14 is now adding support for overloading compound assignment operators.
Compound assignment operators are a composition of the assignment operator and another operator, e.g., +=
which adds the righthand operand to the lefthand operand and assigns it back to it. Essentially,
a += b;
is just a shorthand notation for
a = a + b;
Because of this, the behavior of compound operators could already be defined by overloading the non-assignment operator in the pair:
public class Backpack
{
public Backpack Clone() =>
new(Capacity)
{
Volume = Volume,
Weight = Weight,
items = [.. items],
};
public void Add(Item item)
{
if (Volume + item.Volume > Capacity)
{
throw new InvalidOperationException("Not enough room in backpack.");
}
Volume += item.Volume;
Weight += item.Weight;
items.Add(item);
}
public static Backpack operator +(Backpack oldBackpack, Item item)
{
var newBackpack = oldBackpack.Clone();
newBackpack.Add(item);
return newBackpack;
}
}
This is enough to make the following code work:
var backpack = new Backpack(10);
var item = new Item(2, 5);
backpack += item;
The compiler simply uses the overloaded +
operator. However, this means that a new instance of the Backpack
is created every time, which you might not expect to happen when using the compound operator. For reference types, this also means that the previous instance has to be garbage collected, which can have noticeable performance implications.
In C# 14, you can now define a separate overload for the compound operator:
public class Backpack(float capacity)
{
public void operator +=(Item item)
{
Add(item);
}
}
Notice how the operator is an instance member, not a static member of the class, so that you can access its other instance members and hence modify its state.
When the compound operator is defined, it's going to be used instead of the standalone operator when using the compound operator:
backpack += item;
Of course, the standalone operator is still going to be used when invoking it directly:
var backpack1 = backpack + item;
This means that you usually want to also overload the standalone operator when you're overloading the compound operator. Otherwise, you won't be able to use the standalone operator, which would be unexpected for anyone consuming your class.
Even more importantly, you want the standalone operator and the compound operator to behave identically (except that the former creates a new instance and the latter doesn't). It would be even more unexpected for the consumers of the class, if their behaviors differed. This is something to pay attention to, especially because it might not always be easy to reuse the same code for both operators since one creates a new instance and the other one doesn't.
Two more things worth mentioning:
- You can define a checked variant of a compound operator overload.
- You can define the compound operators as an extension member.
Compound operator overloads work since .NET 10 preview 5 if you set the language version for your project to preview:
<PropertyGroup>
<LangVersion>preview</LangVersion>
</PropertyGroup>
If you have the latest version (17.14.13) of Visual Studio 2022 installed, you can try out the feature yourself. Just ignore the bogus compiler error in the editor window. The code will compile fine.
You can find a sample project with overloaded compound operators in my GitHub repository. Check the tests to see how having the compound operator overloaded prevents the creation of a new instance.
In my opinion, overloading of compound operators is a feature which most of us will rarely use. However, if you already use operator overloading, you might be able to optimize performance without having to change the consumer code at all. This is especially important in cases when overloaded operators are a part of a class library or NuGet package.