Evil code – overload resolution workaround

Another quick break from asynchrony, because I can’t resist blogging about this thoroughly evil idea which came to me on the train.

Your task: to write three static methods such that this C# 4 code:

static void Main()
{
    Foo<int>();
    Foo<string>();
    Foo<int?>();
}

resolves one call to each of them – and will act appropriately for any non-nullable value type, reference type, and nullable value type respectively.

You’re not allowed to change anything in the Main method above, and they have to just be methods – no tricks using delegate-type fields, for example. (I don’t know whether such tricks would help you or not, admittedly. I suspect not.) It can’t just call one method which then determines other methods to call at execution time – we want to resolve this at compile time.

If you want to try this for yourself, look away now. I’ve deliberately included an attempt which won’t work below, so that hopefully you won’t see the working solution accidentally.

The simple (but failed) attempt

You might initially want to try this:

class Test
{
    static void Foo<T>() where T : class {}
    
    static void Foo<T>() where T : struct {}
    
    // Let’s hope the compiler thinks this is "worse"
    // than the others because it has no constraints
    static void Foo<T>()
    
    static void Main()
    {
        Foo<int>();
        Foo<int?>();
        Foo<string>();
    } 
}

That’s no good at all. I wrote about why it’s no good in this very blog, last week. The compiler only checks generic constraints on the type parameters after overload resolution.

Fail.

First steps towards a solution

You may remember that the compiler does check that parameter types make sense when working out the candidate set. That gives us some hope… all we’ve got to do is propagate our desired constraints into parameters.

Ah… but we’re calling a method with no arguments. So there can’t be any parameters, right?

Wrong. We can have an optional parameter. Okay, now we’re getting somewhere. What type of parameter can we apply to force a parameter to only be valid if a generic type parameter T is a non-nullable type? The simplest option which occurs is to use Nullable<T> – that has an appropriate constraint. So, we end up with a method like

static void Foo<T>(T? ignored = default(T?)) where T : struct {}

Okay, so that’s the first call sorted out – it will be valid for the above method, but neither of the others will.

What about the reference type parameter? That’s slightly trickier – I can’t think of any common generic types in the framework which require their type parameters to be reference types. There may be one, of course – I just can’t think of one offhand. Fortunately, it’s easy to declare such a type ourselves, and then use it in another method:

class ClassConstraint<T> where T : class {}
    
static void Foo<T>(ClassConstraint<T> ignored = default(ClassConstraint<T>)) 
    where T : class {}

Great. Just one more to go. Unfortunately, there’s no constraint which only satisfies nullable value types… Hmm.

The awkwardness of nullable value types

We want to effectively say, "Use this method if neither of the other two work – but use the other methods in preference." Now if we weren’t already using optional parameters, we could potentially do it that way – by introducing a single optional parameter, we could have a method which was still valid for the other calls, but would be deemed "worse" by overload resolution. Unfortunately, overload resolution takes a binary view of optional parameters: either the compiler is having to fill in some parameters itself, or it’s not. It doesn’t think that filling in two parameter is "worse" than only filling in one.

Luckily, there’s a way out… inheritance to the rescue! (It’s not often you’ll hear me say that.)

The compiler will always prefer applicable methods in a derived class to applicable methods in a base class, even if they’d otherwise be better. So we can write a parameterless method with no type constraints at all in a base class. We can even keep it as a private method, so long as we make the derived class a nested type within its own base class.

Final solution

This leads to the final code – this time with diagnostics to prove it works:

using System;

class Base
{
    static void Foo<T>()
    {
        Console.WriteLine("nullable value type");
    }

    class Test : Base
    {
        static void Foo<T>(T? ignored = default(T?))
            where T : struct
        {
            Console.WriteLine("non-nullable value type");
        }
        
        class ClassConstraint<T> where T : class {}
        
        static void Foo<T>(ClassConstraint<T> ignored = default(ClassConstraint<T>)) 
            where T : class
        {
            Console.WriteLine("reference type");
        }
        
        static void Main()
        {
            Foo<int>();
            Foo<string>();
            Foo<int?>();
        }
    }
}

And the output…

non-nullable value type
reference type
nullable value type

Conclusion

This is possibly the most horrible code I’ve ever written.

Please, please don’t use it in real life. Use different method names or something like that.

Still, it’s a fun little puzzle, isn’t it?

3 thoughts on “Evil code – overload resolution workaround”

  1. This is pretty cool.

    “Please, please don’t use it in real life. Use different method names or something like that.”

    Point Taken.

    “Still, it’s a fun little puzzle, isn’t it?”

    Yes, indeed.

    Thanks :)

  2. Intriguing – there’s something going on with the overload resolver in the absence of optional parameters that I need to give some more thought to.

    I’m trying to figure out whether the analagous case with type inference:

    Bar(default(int));
    Bar(default(string));
    Bar(default(int?));

    is any easier to solve, or if the same hoopjumping is required. It certainly works if you take the Foo methods and add a mandatory T parameter, and clearly some sort of optional parameter tomfoolery is needed to make the methods into distinct overloads. I suspect you do still need the constraining types and the baseclass. Interesting.

Comments are closed.