C2392 - Covariant Error from hell
I was moving some code from C# to Managed C++ and needed to port over some collections, while operating under constraints: the code needed to compile cleanly targeting .NET 1.1 and .NET 2.0, and I can't use generics nor can I use C#.
In a collection of PointF, I have this indexer:
__property PointF get_Item(Int32 index)
{
if (index < 0 || index > InnerList->Count - 1)
throw new ArgumentOutOfRangeException(S"index", __box(index), S"Invalid index for this collection.");
return *dynamic_cast<__box PointF *>(InnerList->get_Item(index));
}
which works just great when under those constraints.
Now compare this nearly identical method in a collection of Polygon objects:
__property Polygon * get_Item(Int32 index)
{
if (index < 0 || index > InnerList->Count - 1)
throw new ArgumentOutOfRangeException(S"index", __box(index), S"Invalid index for this collection.");
return dynamic_cast<Polygon *>(InnerList->Item[index]);
}
They are nearly identical and should be. This is boiler plate code and exactly the kind of code for which you want generics or templates (neither of which I can use under the constraints).
The difference between the two is that the second block fails to compile in .NET 2.0 with the dreaded C2392 error. If you read the tech article on MSDN about this, there is no solution except to change your return type to Object *. This is unpalatable in that you lose everything that you want to gain with the collection class in the first place: type safety. I kept getting hung up though: I was looking at one chunk of code that works and one that doesn't. Since they were in different assemblies, I looked for compiler settings that were different and found none. Eventually, by isolating the code in a clean environment, I discovered that the issue is structs vs classes. If get_Item returns a value struct, then the patterns works fine. If get_Item returns a class, you're hosed.
At this point, I tried some experiments and discovered that using the newer syntax for managed C++ allows you to create this pattern just fine, so I resorted to creating a stack of the distasteful macros like all C programmers have had to do at some point to abstract out syntactic differences. My intent was to compile it with one set of managed extensions in .NET 1.1 and another for 2.0. Initial experiments proved fruitful as I started to define things like:
#if DOTNET20
#define GC_CLASS ref class
#else
#define GC_CLASS __gc class
#endif
public GC_CLASS MyClass { ... }
which is all well and good, except that this was the only class to which I wanted to apply the macros and I realized I had another constraint: I had to have /clr:oldSyntax set for the entire project - I couldn't turn it off for just one file.
It finally hit me - the real issue for the error is that get_Item conflicts with an existing definition in IList. The trick is to create an indexer that simply isn't named get_Item:
__property Polygon * get_ASItem(Int32 index)
and then decorate the class with this attribute:
[DefaultMember(S"ASItem")]
This will instruct the compiler to make the indexer go to get_ASItem (or set_ASItem - as appropriate) whenever someone does coll[i] from C#.
There's a bigger lesson in here though, and it has to do with messing with standards. Microsoft chose to mess with the C++ standard in order to make managed C++. This is always a heinous issue - compiler extensions lead to non-portable code or disgusting macro hacking to cover them up. Worse than simply syntax hacking once, the managed C++ team did it twice and created a portability issue, even when the target should be the same. I'm sure they didn't make the decision lightly. The differences in array handling alone make working in managed C++ far less painful - if you can take advantage of it. Unfortunately, that's a big if and I ended up taking one for the team with this issue. Fortunately I found a slightly sleazy workaround which is better than returning losing type safety.