1

I am working on arbitrary object tree conversion to some specific format, using dynamic dispatch (to simplify handling generics). The object tree is generated by interop code, wraps some values into containers, which have implicit conversions defined on them.

When I process those containers, I hit StackOverflowException. Initially, I thought maybe there is a stupid mistake, where I just call Accept on the same value, however, I managed to narrow down my issue to this:

using System;
using System.Runtime.CompilerServices;

class Program {
    static void Main(string[] args) {
        var container = new Container<object>("42");
        Accept((dynamic)container);            
    }

    static void Accept(object obj) { Console.WriteLine(nameof(obj)); }
    static void Accept(int i32) { Console.WriteLine(nameof(i32)); }

    static void Accept<T>(IContainer<T> container) {
        RuntimeHelpers.EnsureSufficientExecutionStack();
        Accept((dynamic)container.Get());
    }
}

public interface IContainer<out T>{
    T Get();
}

public struct Container<T>: IContainer<object> {
    readonly T value;
    public Container(T value){ this.value = value; }
    public static implicit operator Container<T>(T value) => new Container<T>(value);
    public object Get() => this.value;
}

Surprisingly, EnsureSufficientExecutionStack fails. Apparently, whatever performs dynamic dispatch on the value of container.Get(), wraps the result back into Container<object> and passes the result to Accept<T>(IContainer<T>) instead of passing it directly to Accept(object).

Question is: what logic does it follow, when it chooses to do so? It made slight sense when I had Accept<T>(Container<T>), as I could see it guessing it can call the implicit conversion, but I can't comprehend how does it even find implicit conversion to some arbitrary interface implementation to dispatch to the overload with an interface parameter.

What blew my mind completely was that if in Container<object>? container = new Container<object>("42"); I replace object with string, the error goes away, and the program correctly prints obj

LOST
  • 2,956
  • 3
  • 25
  • 40
  • Another crazy thing: if I duplicate `struct Container` definition into `struct Container2`, it still crashes with stack overflow, despite now having a clear ambiguity about possible implicit conversions. – LOST Dec 04 '18 at 09:10
  • When you do first `Accept((dynamic)container)` it wrap `container` within another `Container` before call. I can not explain this behavior. All following `Accept` calls do the same, because you call it with the same value. – user4003407 Dec 04 '18 at 19:09
  • @PetSerAl, huh, that's the actual mystery there! Why does it wrap? How does it even know how to wrap in a presence of `Container2`? – LOST Dec 04 '18 at 19:22
  • BTW, for now I worked around this issue by replacing all `Accept((dynamic)...)` with `AcceptDispatch(...)` where `AcceptDispatch(object obj) => obj is IContainer container ? AcceptDispatch(container.Get()) : Accept((dynamic)obj)` – LOST Dec 04 '18 at 19:29
  • 1
    I think it all boils down to [this](https://tio.run/##fVBBTsMwELz3FaucHAkqypGWSqSVuMAJS5xdZ0GLEjvybkBR5LcHp0QhPcAcLI/Hs5pZy9eW7TC0TO4dXjoWrLcrWxlm0MgC/QoSWIyQhU9PJTwbciqfhBFl50yd1BLuweEXHHf@9IFW9irfzp8O3rGvcP0aSPCJHKrsIVvoswlok@aU/zqLpbMAuv11xPP5U6CA/oIfd3p/N77O3qY9VSn5VI/qJjES8A0GIz6cHUqDLOv@kelgxm3dROg3MbsC6Rr0b0rn6bp@RNGJq3wRe0RAaYOblqYv9hWnNnEYvgE). Implicit cast invoke user-defined conversion, instead of build-in. – user4003407 Dec 05 '18 at 10:49
  • `dynamic` aside, the rules for overload resolution make implicit conversion operators that can convert from *any* type a really bad idea. Make the operator explicit and your problems disappear (or make a non-static `Container` class with a static `Wrap` method so you can benefit from type inference). – Jeroen Mostert Dec 05 '18 at 11:00
  • @PetSerAl question is: is this by design, or is it broken? If it is by design, then what rule does it follow? – LOST Dec 05 '18 at 18:53

0 Answers0