Wednesday, July 18, 2007

C++/CLI is disgusting

Microsoft has found a truly awful set of syntax and semantics for their new C++/CLI language, formerly known as "Managed Extensions for C++". They decided that the old syntax was ugly because it used keywords that began with double underscores (which is a standard way to add compiler extensions in C++). Unfortunately, their solution was much worse than the problem they were trying to solve.

I had been using the first Managed C++ for a little while, but luckily I only made a single module in it (wrapper classes to allow C# to access some C++ classes). After a couple years I wanted to add a dialog box that accessed the C++ classes directly, but the forms designer only supported the "new" syntax; worse, Microsoft requires that the entire project only use one syntax or the other. So I learned the awfulness of the new design as I laboriously converted each line of the old code to the new syntax; the new syntax is so different that virtually every line of the module's header file had to be changed. And not just slightly. In many cases it was faster to retype the line than to try to adjust it. And they didn't just make new syntax, they invented new problematic semantics as well.

The changes include
  • The new "handles". A pointer to a managed class used to be called MyClass*, now it's MyClass^. Other than that they are still used like pointers (i.e. with the arrow notation).
  • "Tracking references". Instead of writing String^& and Int32& like you would expect, you have to write String^% and Int32%.
  • nullptr. Whereas you used to be able to initialize all pointers to NULL, including managed pointers, now you have to remember if it's a managed class and use "nullptr" if so.
  • Same with 'new'; now you have to write gcnew if the class is managed.
  • New finalizer syntax. Confusingly, whereas C# and Managed C++ use "~ClassName" for finalizers, Microsoft decided it was too predictable and renamed it to "!ClassName". Worse, they now require your Dispose() function to be called ~ClassName(), which causes a silent semantics change in old code. Or it would, except that you'll know something's up because your Dispose() method yields this odd error: "'Dispose' : this method is reserved within a managed class".
  • You can no longer use a managed enum like you do a normal enum; you have to qualify the names with "EnumName::EnumValue". This makes it impossible to share an enum between C# and standard C++ code, so you have to create a second enum (with the same items) and convert between them all the time.
  • In a managed class you must say if you're overriding a base class function or not, or you'll get a compiler error--whereas in pure standard C++ you can't. Argh! Even C# lets you off with a warning. And what a bizarre syntax they've picked too; instead of grouping the "override" keyword with "static", "virtual", etc., they make you put it at the end: virtual void foo() override {}. What's more, you have to specify both virtual and override.
  • Similarly, "sealed" and "abstract" go after the class name.
  • When making managed properties, you now have to group the setter with the getter in a single construct like in C#, but unlike in C#, you also have to repeat the data type three times (or twice if it's just a getter). How many times do you want to type Dictionary<string,SomeFreakyLongClassName>?
  • CLR enums are no longer implicitly convertable to arithmetic types.
  • They've switched to the standard 'typeid' syntax instead of __typeof(MyManagedType). Oh wait, no they haven't! The syntax is randomly different: MyManagedType::typeid versus typeid(UnmanagedType).
  • What the hell were they thinking here?
    virtual Object^ InterfaceClone() = ICloneable::Clone;

    The old syntax for 'explicit interface implementation' made much more sense:
    Object* ICloneable::Clone();
Tell me, how is it that when C# is supposedly modeled after C++, the C++ version of all these .NET features ends up looking so much longer different than C#?

Admittedly, there are a few things that don't suck, like
  • the support for normal C++-style operator overloading in managed classes.
  • implicit boxing (although if NULL is defined as 0, watch out for boxed zeros when converting old code)
  • default indexers (much like in C#)
  • trivial properties (but they're inflexible and so not usable in many scenarios)
And now some managed-style features work in unmanaged classes, such as properties. Personally I have no use for this. After all, using such features means you can't compile your unmanaged class in a non-.NET program, so their utility is limited. If I want to write a class that only works in .NET, I would almost always make it a "ref class" or "ref struct" so I can interoperate with other .NET languages.

There are two main problems I see with their design.

The first big problem is that they've forgotten the spirit of C++ and discarded longstanding rules of C++ such as implicit overriding. C++'s philosophy has long been that an object should be able to behave like a pointer, like a number, like a function. Smart pointers, iterators, fixed-point/matrix classes/bigints, functors. The ability of one thing to act like something else is the whole basis for the STL. But in Microsoft's new design, everything managed is completely segregated so you can no longer write code that doesn't care whether something is managed or not. It's not just reference types either; value types and even simple enums are segregated to an extent that they weren't before. You always have to think: Do I have to Qualify:: that enum or not? should I use gcnew or new here? NULL or nullptr? * or ^? & or %? I can only use one or the other in a given context, but the wretched compiler still makes me tell it what it wants to hear. Template code that before could have (theoretically) taken managed or unmanaged classes for arguments can now take only one or the other, because a separate syntax is needed for each.

The second big problem is that there is no longer anything I can share between C# and standard C++. I have a library that needs to be compiled into both C# programs and MFC programs (which must be Windows CE compatible, so mixing .NET and MFC is not an option). With the old syntax it was possible to share a small number of value types and enums between plain C++ and managed C++ (with the help of some #define macros); now I have to make two versions and convert between them.

If anything, Microsoft should have made the managed syntax more like standard C++, not less. It should have considered how to allow people to write classes that could be used directly from C# or (in another program) directly from standard C++. This would have made a much better bridge between unmanaged land and managed land. As it is, Microsoft has imposed a kind of syntax apartheid.

Bottom line: I loathe the new syntax. It makes me long for the hellish landscape of double underscores again.

2 comments:

Anonymous said...

That's strange - I had a similar level of hatred for C++.NET (or was it MC++?) and expected to C++/CLI to just be more annoying but I just today took some files from a C++ project and added them to a new C++/CLI app, stl and all, and it literally compiled and ran. I think this language has just blown open a new world for me! Actually the value/stack semantics for managed objects is what did it I think.

Qwertie said...

Oh, you're talking about running normal C++ code in the CLR. That's called "It Just Works" (IJW) and I agree it's very cool (and it proves how capable the .NET platform is). But the syntax for "normal" C#-compatible .NET code is terrible, IMO, for the reasons stated.