Edward Diener asked:
That's one way to design it. Interestingly, the existing "Managed C++" does use just
plain old new for everything, with pretty much the semantics described
above: In MC++, the expression new T allocates a T object
on the CLI gc heap if T is a CLI type, and on the native
heap if T is a native type.
Briefly, this model was limiting, and it was confusing to users. Just to give a taste
for what happens when you go down this path, the next question you hit is "what is a T*?"
In particular, consider:
T* t = new T;
Is t a pointer to an object in gc'd memory, which means that the
object can move? Or is it a pointer to an object in native memory that doesn't
move? "That's easy!" one might say. "We could deduce that t points
into gc'd memory if and only if T is a CLI type." In the above code
statement, that's true, and Managed C++ used "defaulting rules" to make it mean exactly
that. In particular, in MC++ the T* above means the same as the following
longer version if T is a CLI type:
T __gc * t = __gc new T;
But it turns out that when going along this path you do need that __gc pointer
decoration (or its moral equivalent) sometimes, even if much of the time you can make
it optional by defaulting it based on the pointed-at type. In particular, you need
it for combinations of pointers, including the simplest case:
int** ppi; // what is this?
Consider: Where is the int* that the int** is pointing
to? The answer cannot be deduced from the type, because an int* can
exist on the gc'd heap or on the native heap. Therefore both int* __gc * (a
pointer to an int* on the gc heap) and int** (a
pointer to an int* on the native heap) are valid pointer types, and therefore
you need to be able to distinguish between them.
In practice, this has been a great source of user confusion because programmers
are never really sure where to put the __gc. So what we have observed
many users do, time and again, is simply add __gc's until the code
compiles -- whether the __gc is needed (or correct) or not.
So you can use defaulting rules to hide the __gc some of the time,
but not all of the time. And the "some of the time" itself is at a cost, namely that
it arbitrarily restricts native types from ever being allocated on the gc heap (i.e.,
from being garbage collected), and restricts CLI types from ever being allocated on
the native heap.
Bingo. In addition to the clarity and usability issue above, you hit on a primary
reason for not just using unadorned new: It conflates two ideas that
ought to be independent, namely the idea of what kind of type T is
and the idea of where T objects are allocated. In particular, we
will definitely in the future allow allocating an object of any type on the gc heap
or on the native heap.
Why do we feel the need to support allocating an object of native type on the GC heap?
Because customers ask for it. "You've got a great garbage collector in there," they
say, "so why can't I use it to garbage-collect the objects I already have today?"
That's a reasonable question.
Why do we feel the need to support allocating an object of CLI type on the native
heap? Because there are lots of native templated libraries out there today, many of
which that internally allocate objects on the native heap (because, after all,
they know nothing about the GC heap). We ought to be able to leverage all those
existing libraries and use them unmodified also with the CLI types. True, for
some such libraries it may be enough simply to give a different meaning to new depending
on the type of T as originally proposed above. But note that
such libraries might not only allocate objects using new, but may
rely on the resulting pointers to support things like pointer arithmetic that MC++'s __gc*'s
and C++/CLI's ^'s cannot support, and so they would still be broken
for CLI types.
Of the two, I see gc'ing native objects as the more compelling and mainstream use