Is there any way in C# to enforce operator overloading in derived classes?

Viewed 13274

I need to define an Interface which has to enforce certain operator overloading to the types which implements it. There doesn't seem an obvious way to do it since operator overloading has to be done using static methods in class. Is there any way to achieve the same effect (using abstract classes or anything else)?

7 Answers

Is there any way in C# to enforce operator overloading in derived classes?

Strictly speaking, types don't "derive" from an interface, they only implement it - but if you are referring to requiring operator overloads in a derived class from a parent class then that can be done by making the parent class generic to allow for variant operator parameter and return types, but this effectively means that the subclass cannot then be subclassed again (without making the subclass generic).

Is there any way to achieve the same effect (using abstract classes or anything else)?

Now, assuming that you are referring to only interfaces, then yes, it's possible! The trick is to not have the operators defined on the interface nor it classes, but in a wrapper-struct - which is used through the magic of operator implicit...

There is a slight catch though, but which is entirely solvable provided you're using C# 6.0 or later... read on!


  • What you're describing is (another) good use-case for wrapper-structs over open generic interfaces.

    • This is basically the same thing as Mads Torgersen's suggested approach for implementing "extension-everything": where any object (class) or value (struct), including any interface, is wrapped in a struct invisibly to your program, and that wrapper-struct then adds the required functionality you're after.
    • ...in this case, you want to add "extension operators" to types that will implement an existing interface that defines the underling operations for those operators.
    • Remember that value-types (struct etc) are "free" in .NET because they don't incur a GC heap allocation.
  • To make this work, first define the interface with the operations you want to support.

    • This interface has generic type parameters to allow for variant return types, and for accessing the "raw value":
public interface IOperable<TImpl,TValue> : IEquatable<TImpl>, IComparable<TImpl>
    where TImpl  : IOperable<TImpl,TValue>
    where TValue : notnull
{
    Operable<TImpl,TValue> Op { get; }
    
    TImpl CreateFor( TValue other );
    
    TImpl Self  { get; }
    
    TValue Value { get; }
    
    TImpl Add( TValue other );
    
    TImpl Subtract( TValue other );
    
    TImpl Multiply( TValue other );
    
    TImpl Divide( TValue other );
    
    TImpl Remainder( TValue other );
    
    TImpl Inverse();
}
  • Then define the wrapper-struct (struct Operable<TImpl,TValue>).
    • The wrapper-struct has the static operator methods which all accept and return the the same Operable<TImpl,TValue>.
    • But most importantly: struct Operable also has implicit operator defined for implicit conversion to-and-from TImpl and to TValue, which is what helps make this approach usable.
    • Implicit conversion from TValue isn't possible because there's no way of knowing what TImpl would be, but you can define operators on struct Operable<> that allow raw TValue for either of the operands, that way it can infer TImpl from the other operand.
// Note that `struct Operable<T>` does not implement IOperable<T>.
public struct Operable<TImpl,TValue>
    where TImpl  : IOperable<TImpl,TValue>
    where TValue : notnull
{
    #region Operators (Self-to-Self)
    
    public static Operable<TImpl,TValue> operator +(Operable<TImpl,TValue> lhs, Operable<TImpl,TValue> rhs)
    {
        TImpl result = lhs.Self.Add( rhs.Value );
        return result;
    }
    
    public static Operable<TImpl,TValue> operator -(Operable<TImpl,TValue> lhs, Operable<TImpl,TValue> rhs)
    {
        return lhs.Self.Subtract( rhs.Value );
    }
    
    public static Operable<TImpl,TValue> operator -(Operable<TImpl,TValue> self)
    {
        return self.Self.Inverse();
    }
    
    public static Operable<TImpl,TValue> operator *(Operable<TImpl,TValue> lhs, Operable<TImpl,TValue> rhs)
    {
        return lhs.Self.Multiply( rhs.Value );
    }
    
    public static Operable<TImpl,TValue> operator /(Operable<TImpl,TValue> lhs, Operable<TImpl,TValue> rhs)
    {
        return lhs.Self.Divide( rhs.Value );
    }
    
    public static Operable<TImpl,TValue> operator %(Operable<TImpl,TValue> lhs, Operable<TImpl,TValue> rhs)
    {
        return lhs.Self.Remainder( rhs.Value );
    }
    
    #endregion
    
    #region Operators (Self + TValue)
    
    public static Operable<TImpl,TValue> operator +(Operable<TImpl,TValue> lhs, TValue rhs)
    {
        TImpl result = lhs.Self.Add( rhs );
        return result;
    }
    
    public static Operable<TImpl,TValue> operator -(Operable<TImpl,TValue> lhs, TValue rhs)
    {
        return lhs.Self.Subtract( rhs );
    }
    
    public static Operable<TImpl,TValue> operator *(Operable<TImpl,TValue> lhs, TValue rhs)
    {
        return lhs.Self.Multiply( rhs );
    }
    
    public static Operable<TImpl,TValue> operator /(Operable<TImpl,TValue> lhs, TValue rhs)
    {
        return lhs.Self.Divide( rhs );
    }
    
    public static Operable<TImpl,TValue> operator %(Operable<TImpl,TValue> lhs, TValue rhs)
    {
        return lhs.Self.Remainder( rhs );
    }
    
    #endregion
    
    #region Operators (TValue + Self)
    
    public static Operable<TImpl,TValue> operator +(TValue lhs, Operable<TImpl,TValue> rhs)
    {
        TImpl lhs2 = rhs.Self.CreateFor( lhs );
        TImpl result = lhs2.Self.Add( rhs.Value );
        return result;
    }
    
    public static Operable<TImpl,TValue> operator -(TValue lhs, Operable<TImpl,TValue> rhs)
    {
        TImpl lhs2 = rhs.Self.CreateFor( lhs );
        TImpl result = lhs2.Self.Subtract( rhs.Value );
        return result;
    }
    
    public static Operable<TImpl,TValue> operator *(TValue lhs, Operable<TImpl,TValue> rhs)
    {
        TImpl lhs2 = rhs.Self.CreateFor( lhs );
        TImpl result = lhs2.Self.Multiply( rhs.Value );
        return result;
    }
    
    public static Operable<TImpl,TValue> operator /(TValue lhs, Operable<TImpl,TValue> rhs)
    {
        TImpl lhs2 = rhs.Self.CreateFor( lhs );
        TImpl result = lhs2.Self.Divide( rhs.Value );
        return result;
    }
    
    public static Operable<TImpl,TValue> operator %(TValue lhs, Operable<TImpl,TValue> rhs)
    {
        TImpl lhs2 = rhs.Self.CreateFor( lhs );
        TImpl result = lhs2.Self.Remainder( rhs.Value );
        return result;
    }
    
    #endregion
    
    public static implicit operator Operable<TImpl,TValue>( TImpl impl )
    {
        return new Operable<TImpl,TValue>( impl );
    }
    
//  public static implicit operator TValue( Operable<TImpl,TValue> self )
//  {
//      return self.Value;
//  }
    
    public static implicit operator TImpl( Operable<TImpl,TValue> self )
    {
        return self.Self;
    }
    
    public Operable( TImpl impl )
    {
        this.Self  = impl;
        this.Value = impl.Value;
    }
    
    public TImpl  Self  { get; }
    public TValue Value { get; }
}

So for example, supposing we have a custom number type that we want to enforce at compile-time the operators described above...

public struct ComplexNumber
{
    public Double Real;
    public Double Complex;
}

Simply make it implement IOperable, so this implementation defines most arithmetic operations on complex numbers:

public struct ComplexNumber : IOperable<ComplexNumber,ComplexNumber>
{
    public static implicit operator ComplexNumber( ( Double r, Double i ) valueTuple )
    {
        return new ComplexNumber( valueTuple.r, valueTuple.i ); 
    }
    
    public Double Real;
    public Double Imaginary;

    public ComplexNumber( Double real, Double imaginary )
    {
        this.Real      = real;
        this.Imaginary = imaginary;
    }
    
    public Double Magnitude => Math.Sqrt( ( this.Real * this.Real ) + ( this.Imaginary * this.Imaginary ) );
    
    public ComplexNumber Conjugate => new ComplexNumber( real: this.Real, imaginary: -this.Imaginary );

    public ComplexNumber Self => this;
    public ComplexNumber Value => this;
    
    public Operable<ComplexNumber,ComplexNumber> Op => new Operable<ComplexNumber,ComplexNumber>( this.Value );

    public ComplexNumber Add(ComplexNumber other)
    {
        Double r = this.Real      + other.Real;
        Double i = this.Imaginary + other.Imaginary;
        return new ComplexNumber( r, i ); 
    }
    
    public ComplexNumber Subtract(ComplexNumber other)
    {
        Double r = this.Real      - other.Real;
        Double i = this.Imaginary - other.Imaginary;
        return new ComplexNumber( r, i ); 
    }

    public ComplexNumber Multiply(ComplexNumber other)
    {
        // (a+bi) * (c+di) == a(c + di) + bi(c + di)
        //                 == (ac - bd) + (ad + bc)i
        
        Double a = this.Real;
        Double b = this.Imaginary;
        
        Double c = other.Real;
        Double d = other.Imaginary;
        
        //
        
        Double r = ( a * c ) - ( b * d );
        Double i = ( a * d ) + ( b * c );
        return new ComplexNumber( r, i );
    }
    
    public ComplexNumber Divide(ComplexNumber other)
    {
        // Division is the same as multiplying by the conjugate.
        
        ComplexNumber conjugate = other.Conjugate;
        
        ComplexNumber numerator   = this.Value.Multiply( conjugate );
        ComplexNumber denominator = other.Multiply( conjugate );
        
        if( denominator.Imaginary == 0 )
        {
            Double d = denominator.Real;
            
            Double newReal = numerator.Real      / d;
            Double newImag = numerator.Imaginary / d;
            
            return new ComplexNumber( newReal, newImag );
        }
        else
        {
            throw new NotSupportedException( "Non-trivial complex division is not implemented." );
        }
    }

    public ComplexNumber Remainder(ComplexNumber other)
    {
        // Remainder isn't the same as Modulo (fun-fact: in C89 the `%` operator is for remainder, not modulo!)
        // Anyway, implementing Remainder for complex numbers is non-trivial.
        // As is Modulo: https://math.stackexchange.com/questions/274694/modulo-complex-number
        // So just throw:
        
        throw new NotSupportedException( "The remainder operation for complex-numbers is not implemented." );
    }
    
    public ComplexNumber Inverse()
    {
        return new ComplexNumber( real: -this.Real, imaginary: -this.Imaginary );
    }
    
    #region IEquatable + IComparable
    
    public ComplexNumber CreateFor(ComplexNumber other)
    {
        return other;
    }
    
    public Int32 CompareTo( ComplexNumber other )
    {
        return this.Magnitude.CompareTo( other.Magnitude );
    }

    public override Boolean Equals( Object? obj )
    {
        return obj is ComplexNumber other && this.Equals( other: other );
    }

    public override Int32 GetHashCode()
    {
        return base.GetHashCode();
    }

    public Boolean Equals( ComplexNumber other )
    {
        return this.Real == other.Real && this.Imaginary == other.Imaginary;
    }

    public override String ToString()
    {
        if( this.Imaginary < 0 )
        {
            return String.Format( CultureInfo.InvariantCulture, "({0}{1}i)", this.Real, this.Imaginary );
        }
        else
        {
            return String.Format( CultureInfo.InvariantCulture, "({0}+{1}i)", this.Real, this.Imaginary );
        }
    }

    #endregion
}

So this should be able to be used like so:

public static void Main()
{
    ComplexNumber a = ( r: 6, i:  4 );
    ComplexNumber b = ( r: 8, i: -2 );
    
    ComplexNumber c = a + b;
    
    Console.WriteLine( "{0} + {1} = {2}", a, b, c );
}

...but it doesn't!

The problem is that we need either a or b to be implicitly promoted to Operable<ComplexNumber,ComplexNumber> so that the overloaded + operator will be invoked.

The quick-and-dirty workaround is to use that Op property on the innermost operand (according to operator precedence rules) to trigger the implicit conversion to Operable<> and the compiler takes care of the rest, including implicit conversion back to ComplexNumber:

So this:

public static void Main()
{
    ComplexNumber a = ( r: 6, i:  4 );
    ComplexNumber b = ( r: 8, i: -2 );
    
    ComplexNumber c = a.Op + b;
    
    Console.WriteLine( "{0} + {1} = {2}", a, b, c );
}

...gives me the expected output of (6+4i) + (8--2i) = (14+2i).

...which then works with expressions of any length and complexity, just remember to use .Op on the first operation, not the left-most (in this case, both b.Op and d.Op due to them being independent operations:

public static void Main()
{
    ComplexNumber a = ( r:  6, i:  4 );
    ComplexNumber b = ( r:  8, i: -2 );
    ComplexNumber c = ( r:  1, i:  9 );
    ComplexNumber d = ( r:  9, i:  5 );
    ComplexNumber e = ( r: -2, i: -1 );
    
    ComplexNumber f = a + b.Op * c - ( d.Op / e );
    
    Console.WriteLine( "{0} + {1} * {2} - ( {3} / {4} ) = {5}", a, b, c, d, e, f );
}

Of course, the .Op part is still an ugly wart, but what can be done about that?

Well, the answer lies in two parts:

  1. A Roslyn code analysis type that verifies that any type that implements IOperable also has the operators overloaded.
    • This is more of a solution to your original question: a way to "enforce" that a type that implements an interface also overloads the operators.
    • This isn't perfect, though: as a type in an external assembly could still be legally compiled without the operators overloaded. Though at least with struct Operand you can still take advantage of overloaded operators (albeit with the .Op wart)
  2. Use Roslyn code-generation to auto-generate the necessary operators in a partial type provided elsewhere, including auto-generating operator implicit to accommodate any external types that haven't overloaded the operators.

Part 1 is straightforward, here's a simple Roslyn analyzer that will raise a warning (or error, at your discretion):

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace You
{
    [DiagnosticAnalyzer( LanguageNames.CSharp )]
    public class OperatorOverloadingAnalyzer : DiagnosticAnalyzer
    {
        public static ImmutableArray<String> FixableDiagnosticIds { get; } = ImmutableArray.Create( "0xDEADBEEF0001" );

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => OperatorOverloadingAnalyzerInfo.AsArray;

        public override void Initialize( AnalysisContext context )
        {
            context.ConfigureGeneratedCodeAnalysis( GeneratedCodeAnalysisFlags.None );
            context.EnableConcurrentExecution();

            context.RegisterSymbolAction( AnalyzeSymbol, SymbolKind.NamedType );
        }

        private static void AnalyzeSymbol( SymbolAnalysisContext context )
        {
            if( IsClassOrStructThatImplementsIOperable( context.Symbol, out INamedTypeSymbol? namedTypeSymbol ) )
            {
                if( !HasOperators( namedTypeSymbol ) )
                {
                    Diagnostic diagnostic = Diagnostic.Create( OperatorOverloadingAnalyzerInfo.Descriptor, location: namedTypeSymbol.Locations[0], namedTypeSymbol.Name );

                    context.ReportDiagnostic( diagnostic );
                }
            }
        }

        private static Boolean IsClassOrStructThatImplementsIOperable( ISymbol symbol, [NotNullWhen(true)] out INamedTypeSymbol? namedTypeSymbol )
        {
            if( symbol is INamedTypeSymbol ins )
            {
                namedTypeSymbol = ins;

                if( namedTypeSymbol.TypeKind == TypeKind.Class || namedTypeSymbol.TypeKind == TypeKind.Struct )
                {
                    if( namedTypeSymbol.AllInterfaces.Any( iface => iface.Name == "IOperable" ) )
                    {
                        return true;
                    }
                }

                return false;
            }
            else
            {
                namedTypeSymbol = null;
                return false;
            }
        }

        private static readonly HashSet<String> _opMemberNames = new HashSet<String>( StringComparer.Ordinal )
        {
            "op_Addition",
            "op_Division",
            "op_Multiply",
            "op_Subtraction"
        };

        private static Boolean HasOperators( INamedTypeSymbol type )
        {
            Int32 matchCount = 0;
            foreach( String memberName in type.MemberNames )
            {
                if( _opMemberNames.Contains( memberName ) )
                {
                    matchCount++;
                }
            }

            return matchCount == 4;
        }
    }
}

Just copy and paste the above into the stock Roslyn analyzer project in VS's project templates and off y'go.


Part 2... is too much effort for me now, so consider that an exercise for the reader.

I'd do something like:

public abstract class Scalar<This> where This : Scalar<This>
{
    public static This operator +(Scalar<This> a, This b) => a.Add(b);

    public abstract This Add(This another);
    ...
 }

Then you can inherit Scalar as:

public sealed class Rational : Scalar<Rational>
{
    public override Rational Add(Rational another)
    {
      ...
    }

    ...
}

And that's it:

Rational a = ...;
Rational b = ...;
Rational sum = a + b;
Related