The syntax for the declaration of managed types and the creation and
use of objects of these types has been significantly altered from
Managed Extensions for C++ to Visual C++ 2008.
A D V E R T I S E M E N T
This was done to
promote their integration within the ISO-C++ type system. These
changes are presented in detail in the following subsections.
In
This Section
Declaration of a Managed Class Type
Discusses how to declare a managed
class,
struct, or
interface.
Declaration of a CLR Reference Class Object
Discusses how to declare a reference class type
object using a tracking handle.
Declaration of a CLR Array
Explains how to declare and initialize an array.
Changes in Constructor Initialization Order
Discusses key changes in class constructor
initialization order.
Changes in Destructor Semantics
Discusses non-deterministic finalization,
Finalize versus
Dispose, ramifications for
reference objects, and use of an explicit
Finalize.
Note: The discussion of
delegates is deferred until
Delegates and Events in order to present them with event
members within a class, the general topic of
Member Declarations within a Class or Interface.
Declaration of a Managed Class Type
The way to declare a reference class type changed from Managed
Extensions for C++ to Visual C++ 2008.
In Managed Extensions, a
reference class type is prefaced with the __gc
keyword. In the new syntax, the __gc
keyword is replaced by one of two spaced keywords:
ref class or ref
struct. The choice of struct or
class indicates the public (for
struct) or private (for
class) default access level of its
members declared in an initial unlabeled section of the body of the
type.
Similarly, in Managed Extensions, a value class type is prefaced
with the __value keyword. In the new
syntax, the __value keyword is replaced
by one of two spaced keywords: value class
or value struct.
An interface type, in Managed Extensions, was indicated with the
keyword __interface. In the new syntax,
this is replaced with interface class.
For example, the following class declarations in Managed
Extensions:
Copy Code
public __gc class Block {}; // reference class
public __value class Vector {}; // value class
public __interface IFooBar {}; // interface class
Under the new syntax these are equivalently declared as follows:
Copy Code
public ref class Block {}; // reference class
public value class Vector {}; // value class
public interface class IFooBar {}; // interface class
Specifying
the Class as Abstract
Under Managed Extensions, you put the keyword
__abstract before the
class keyword (either before or
after the __gc) to indicate
that the class is incomplete and that objects of the class
cannot be created within the program:
Copy Code
public __gc __abstract class Shape {};
public __gc __abstract class Shape2D: public Shape {};
Under the new syntax, you specify the
abstract contextual keyword
following the class name and before either the class body,
base class derivation list, or semicolon.
Copy Code
public ref class Shape abstract {};
public ref class Shape2D abstract : public Shape{};
Of course, the semantic meaning is unchanged.
Specifying
the Class as Sealed
Under Managed Extensions, you put the keyword
__sealed before the
class keyword (either before or
after __gc) to indicate that
objects of the class cannot be inherited from:
Copy Code
public __gc __sealed class String {};
Under the new syntax, you specify the
sealed contextual keyword
following the class name and before either the class body,
base class derivation list, or semicolon.
You can both derive a class and seal it. For example, the
String class is implicitly derived
from Object. The benefit of
sealing a class is that it supports the static resolution
(that is, at compile-time) of all virtual function calls
through the sealed reference class object. This is because
the sealed specifier guarantees
that the String tracking handle
cannot refer to a subsequently derived class that might
provide an overriding instance of the virtual method being
invoked. Here is an example of a sealed class in new syntax:
Copy Code
public ref class String sealed {};
One can also specify a class as both abstract and sealed
� this is a special condition that indicates a static class.
This is described in the CLR documentation as follows:
"A type that is both abstract
and sealed should have only
static members, and serves as what some languages call a
namespace."
For example, here is a declaration of an abstract sealed
class using the Managed Extensions syntax:
Copy Code
public __gc __sealed __abstract class State {
public:
static State() {}
static bool inParamList();
private:
static bool ms_inParam;
};
and here is this declaration translated into the new
syntax:
Copy Code
public ref class State abstract sealed {
public:
static State();
static bool inParamList();
private:
static bool ms_inParam;
};
CLR
Inheritance: Specifying the Base Class
Under the CLR object model, only public single
inheritance is supported. However, Managed Extensions
retained the ISO-C++ default interpretation of a base class
without an access keyword as specifying a private
derivation. This meant that each CLR inheritance declaration
had to provide the public
keyword to override the default interpretation.
Copy Code
// Managed Extensions: error: defaults to private derivation
__gc class Derived : Base {};
In the new syntax definition, the absence of an access
keyword indicates a public derivation in a CLR inheritance
definition. Thus, the public
access keyword is now optional. While this does not require
any modification of Managed Extensions for C++ code, I list
this change here for completeness.
Copy Code
// New syntax: ok: defaults to public derivation
ref class Derived : Base{};
Declaration of a CLR Reference Class Object
The syntax to declare and instantiate an object of a reference class
type has changed from Managed Extensions for C++ to Visual C++ 2008.
In Managed Extensions, a reference class type object is declared by
using the ISO-C++ pointer syntax, with an optional use of the
__gc keyword to the left of the star (*).
For example, here are a variety of reference class type object
declarations under the Managed Extensions syntax:
Under the new syntax, you declare a reference class type object
by using a new declarative token (^)
referred to formally as a tracking handle
and more informally as a hat. (The
tracking adjective means that a reference type sits in the CLR heap,
and can therefore transparently move locations during garbage
collection heap compaction. A tracking handle is transparently
updated during runtime. Two similar concepts are the
tracking reference (%),
and the interior pointer (interior_ptr<>),
discussed in
class="normaltext".
The primary reasons to move the declarative syntax away from a
reuse of the ISO-C++ pointer syntax are as follows:
The use of the pointer syntax did not permit overloaded
operators to be directly applied to a reference object. Rather,
one had to call the operator by using its internal name, such as
rV1->op_Addition(rV2) instead of the
more intuitive rV1+rV2.
A number of pointer operations, such as casting and pointer
arithmetic, not permitted for objects stored on a garbage
collected heap. The notion of a tracking handle better captures
the nature of a CLR reference type.
The __gc modifier on a tracking
handle is unnecessary and is not supported. The use of the object
itself is not changed; it still accesses members through the pointer
member selection operator (->). For
example, here is the previous Managed Extensions code sample
translated into the new syntax:
In Managed Extensions, the existence of two
new expressions to allocate
between the native and managed heap was largely transparent.
In almost all instances, the compiler is able to use the
context to determine whether to allocate memory from the
native or managed heap. For example,
Copy Code
Button *button1 = new Button; // OK: managed heap
int *pi1 = new int; // OK: native heap
Int32 *pi2 = new Int32; // OK: managed heap
When you do not want the contextual heap allocation, you
could direct the compiler with either the
__gc or
__nogc keyword. In the new syntax, the separate
nature of the two new expressions is made explicit with the
introduction of the gcnew
keyword. For example, the previous three declarations look
as follows in the new syntax:
Here is the Managed Extensions initialization of the
Form1 members declared in the
previous section:
Copy Code
void InitializeComponent() {
components = new System::ComponentModel::Container();
button1 = new System::Windows::Forms::Button();
myDataGrid = new DataGrid();
button1->Click +=
new System::EventHandler(this, &Form1::button1_Click);
}
Here is the same initialization recast to the new syntax.
Note that the hat is not required for the reference type
when it is the target of a gcnew
expression.
In the new syntax, 0 no longer
represents a null address but is treated as an integer, the
same as 1, 10,
or 100. A new special token
represents a null value for a tracking reference. For
example, in Managed Extensions, we initialize a reference
type to address no object as follows:
Copy Code
// OK: we set obj to refer to no object
Object * obj = 0;
// Error: no implicit boxing
Object * obj2 = 1;
In the new syntax, any initialization or assignment of a
value type to an Object causes an
implicit boxing of that value type. In the new syntax, both
obj and obj2
are initialized to addressed boxed Int32 objects holding the
values 0 and 1, respectively. For example:
Copy Code
// causes the implicit boxing of both 0 and 1
Object ^ obj = 0;
Object ^ obj2 = 1;
Therefore, in order to perform the explicit
initialization, assignment, and comparison of a tracking
handle to null, use a new keyword,
nullptr. The correct revision of the original example
looks as follows:
Copy Code
// OK: we set obj to refer to no object
Object ^ obj = nullptr;
// OK: we initialize obj2 to a Int32^
Object ^ obj2 = 1;
This complicates somewhat the porting of existing code
into the new syntax. For example, consider the following
value class declaration:
Here, both args and
env are CLR reference types. The
initialization of these two members to 0
in the constructor cannot remain unchanged in the transition
to the new syntax. Rather, they must be changed to
nullptr:
Similarly, tests against those members comparing them to
0 must also be changed to compare
the members to nullptr. Here is
the Managed Extensions syntax:
Here is the revision, replacing each 0
instance with a nullptr. The
translation tool helps in this transformation by automating
many if not all occurrences, including use of the
NULL macro.
The nullptr is converted
into any pointer or tracking handle type but is not promoted
to an integral type. For example, in the following set of
initializations, the nullptr is
valid only as an initial value to the first two.
Copy Code
// OK: we set obj and pstr to refer to no object
Object^ obj = nullptr;
char* pstr = nullptr; // 0 would also work here
// Error: no conversion of nullptr to 0 �
int ival = nullptr;
Similarly, given an overloaded set of methods such as the
following:
An invocation with nullptr
literal, such as the following,
Copy Code
// Error: ambiguous: matches (1) and (2)
f( nullptr );
is ambiguous because the nullptr
matches both a tracking handle and a pointer, and there is
no preference given to one type over the other. (This
situation requires an explicit cast in order to
disambiguate.)
An invocation with 0 exactly
matches instance (3):
Copy Code
// OK: matches (3)
f( 0 );
because 0 is of type integer.
Were f(int) not present, the call
would unambiguously match f(char*)
through a standard conversion. The matching rules give
precedence of an exact match over a standard conversion. In
the absence of an exact match, a standard conversion is
given precedence over an implicit boxing of a value type.
That is why there is no ambiguity.
Declaration of a CLR Array
The syntax for declaring, instantiating, and initializing a managed
array has changed from Managed Extensions for C++ to Visual C++
2008.
The declaration of a CLR array object in Managed Extensions
was an extension of the standard array declaration in which a
__gc keyword was placed between the
name of the array object and its possibly comma-filled dimension, as
in the following pair of examples:
Copy Code
void PrintValues( Object* myArr __gc[]);
void PrintValues( int myArr __gc[,,]);
This has been simplified in the new syntax, in which we use a
template-like declaration similar to the STL
vector declaration. The first parameter indicates the element
type. The second parameter specifies the array dimension (with a
default value of 1, so only multiple dimensions require a second
argument). The array object itself is a tracking handle, and so must
be given a hat. If the element type is also a reference type, it
also requires a hat. For example, the above example, when expressed
in the new syntax, looks as follows:
Copy Code
void PrintValues( array
Because a reference type is a tracking handle rather than an
object, it is possible to specify a CLR array as the return type of
a function. (In contrast, it is not possible to specify the native
array as the return type of a function.) The syntax for doing this
in Managed Extensions was somewhat non-intuitive. For example:
Copy Code
Int32 f() [];
int GetArray() __gc[];
In Visual C++ 2008, the declaration is much simpler. For example,
Copy Code
array^ f();
array^ GetArray();
The shorthand initialization of a local managed array is
supported in both versions of the language. For example:
is considerably simplified in the new syntax (note that because
boxing is implicit in the new syntax, the
__box operator has been eliminated � see
Value Types and Their Behaviors for a discussion):
Because an array is a CLR reference type, the declaration of each
array object is a tracking handle. Therefore, array objects must be
allocated on the CLR heap. (The shorthand notation hides the managed
heap allocation.) Here is the explicit form of an array object
initialization under Managed Extensions:
Copy Code
Object* myArray[] = new Object*[2];
String* myMat[,] = new String*[4,4];
Under the new syntax, the new
expression is replaced with gcnew. The
dimension sizes are passed as parameters to the
gcnew expression, as follows:
Copy Code
array
In the new syntax, an explicit initialization list can follow the
gcnew expression; this was not
supported in Managed Extensions. For example:
Copy Code
// explicit initialization list following gcnew
// was not supported in Managed Extensions
array^ myArray =
gcnew array(4){ 1, 1, 2, 3 };
Changes in Constructor Initialization Order
The order of initialization for class constructors
has changed from Managed Extensions for C++ to
Visual C++ 2008.
Comparison
of Constructor Initialization Order
Under Managed Extensions for C++,
constructor initialization occurred in the
following order:
The constructor of the base class,
if any, is invoked.
The initialization list of the class
is evaluated.
The code body of the class
constructor is executed.
This order of execution follows the same
conventions as in native C++ programming.
The new Visual C++ language prescribes the
following execution order for CLR classes:
The initialization list of the class
is evaluated.
The constructor of the base class,
if any, is invoked.
The code body of the class
constructor is executed.
Note this change applies only to CLR
classes; native classes in Visual C++ 2008
still follow the previous conventions. In
both cases, these rules cascade upward
throughout the whole hierarchy chain of a
given class.
Consider the following code example using
Managed Extensions for C++:
Copy Code
__gc class A
{
public:
A() : _n(1)
{
}
protected:
int _n;
};
__gc class B : public A
{
public:
B() : _m(_n)
{
}
private:
int _m;
};
Following the constructor initialization
order prescribed above, we should see the
following order of execution when new
instances of class B
are constructed:
The constructor of the base class
A is invoked.
The _n member
is initialized to 1.
The initialization list for class
B is
evaluated. The _m
member is initialized to
1.
The code body of class
B is executed.
Now consider the same code in the new
Visual C++ syntax:
Copy Code
ref class A
{
public:
A() : _n(1)
{
}
protected:
int _n;
};
ref class B : A
{
public:
B() : _m(_n)
{
}
private:
int _m;
};
The order of execution when new instances
of class B are
constructed under the new syntax is:
The initialization list for class
B is
evaluated. The _m
member is initialized to
0 (0
is the uninitialized value of the
_m class
member).
The constructor of the base class
A is invoked.
The _n member
is initialized to 1.
The code body of class
B is executed.
Note that a similar syntax produces
different results for these code examples.
The constructor of class
B depends on a value from base class
A to initialize
its member. However, the constructor for
class A has not
yet been invoked. Such a dependency can be
especially dangerous when the inherited
class depends on a memory or resource
allocation to occur in the base class
constructor.
What
This Means Going from Managed Extensions for C++
to Visual C++ 2005
In many cases the changes to the
execution order of class constructors should
be transparent to the programmer because
base classes have no notion of the behavior
of inherited classes. However, as these code
examples illustrate, the constructors of
inherited classes can be greatly affected
when their initialization lists depend on
the values of base class members. When you
move your code from Managed Extensions for
C++ to the new syntax, consider moving such
constructs to the body of the class
constructor, where execution is guaranteed
to occur last.
Changes in Destructor Semantics
Semantics for class destructors have changed significantly from
Managed Extensions for C++ to Visual C++ 2008.
In Managed
Extensions, a class destructor was permitted within a reference
class but not within a value class. This has not changed in the new
syntax. However, the semantics of the class destructor have changed.
This topic focuses on the reasons of that change and discusses how
it affects the translation of existing CLR code. It is probably the
most important programmer-level change between the two versions of
the language.
Non-deterministic
Finalization
Before the memory associated with an object is reclaimed
by the garbage collector, an associated
Finalize method, if present, is invoked. You can
think of this method as a kind of super-destructor because
it is not tied to the program lifetime of the object. We
refer to this as finalization. The timing of just when or
even whether a Finalize method is
invoked is undefined. This is what we mean when we say that
garbage collection exhibits non-deterministic finalization.
Non-deterministic finalization works well with dynamic
memory management. When available memory becomes scarce, the
garbage collector kicks in. Under a garbage collected
environment, destructors to free memory are unnecessary.
Non-deterministic finalization does not work well, however,
when an object maintains a critical resource such as a
database connection or a lock of some sort. In this case, we
should release that resource as soon as possible. In the
native world, that is achieved by using a
constructor/destructor pair. As soon as the lifetime of the
object ends, either when the local block within which it is
declared ends, or when the stack unravels because of a
thrown exception, the destructor executes and the resource
is automatically released. This approach works very well,
and its absence under Managed Extensions was sorely missed.
The solution provided by the CLR is for a class to
implement the Dispose method of
the IDisposable interface. The
problem here is that Dispose
requires an explicit invocation by the user. This is
error-prone. The C# language provides a modest form of
automation in the form of a special
using statement. The Managed Extensions design
provided no special support.
Destructors
in Managed Extensions for C++
In Managed Extensions, the destructor of a reference
class is implemented by using the following two steps:
The user-supplied destructor is renamed internally
to Finalize. If the class has
a base class (remember, under the CLR Object Model, only
single inheritance is supported), the compiler injects a
call to its finalizer following execution of the
user-supplied code. For example, consider the following
simple hierarchy taken from the Managed Extensions
language specification:
Copy Code
__gc class A {
public:
~A() { Console::WriteLine(S"in ~A"); }
};
__gc class B : public A {
public:
~B() { Console::WriteLine(S"in ~B"); }
};
In this example, both destructors are renamed
Finalize. B's
Finalize has an invocation of
A's Finalize
method added following the invocation of
WriteLine. This is what the garbage collector will by
default invoke during finalization. Here is what this
internal transformation might look like:
Copy Code
// internal transformation of destructor under Managed Extensions
__gc class A {
public:
void Finalize() { Console::WriteLine(S"in ~A"); }
};
__gc class B : public A {
public:
void Finalize() {
Console::WriteLine(S"in ~B");
A::Finalize();
}
};
In the second step, the compiler synthesizes a
virtual destructor. This destructor is what our Managed
Extensions user programs invoke either directly or
through an application of the delete expression. It is
never invoked by the garbage collector.
Two statements
are placed within this synthesized destructor. One is a
call to GC::SuppressFinalize
to make sure that there are no more invocations of
Finalize. The second is the
actual invocation of Finalize,
which represents the user-supplied destructor for that
class. Here is what this might look like:
Copy Code
__gc class A {
public:
virtual ~A() {
System::GC::SuppressFinalize(this);
A::Finalize();
}
};
__gc class B : public A {
public:
virtual ~B() {
System::GC::SuppressFinalize(this);
B::Finalize();
}
};
While this implementation allows the user to explicitly
invoke the class Finalize method
now rather than at a time you have no control over, it does
not really tie in with the Dispose
method solution. This is changed in Visual C++ 2008.
Destructors
in New Syntax
In the new syntax, the destructor is renamed internally
to the Dispose method and the
reference class is automatically extended to implement the
IDispose interface. That is, under
Visual C++ 2008, our pair of classes is transformed as
follows:
Copy Code
// internal transformation of destructor under the new syntax
__gc class A : IDisposable {
public:
void Dispose() {
System::GC::SuppressFinalize(this);
Console::WriteLine( "in ~A");
}
};
__gc class B : public A {
public:
void Dispose() {
System::GC::SuppressFinalize(this);
Console::WriteLine( "in ~B");
A::Dispose();
}
};
When either a destructor is invoked explicitly under the
new syntax, or when delete is
applied to a tracking handle, the underlying
Dispose method is invoked
automatically. If it is a derived class, a call of the
Dispose method of the base class
is inserted at the close of the synthesized method.
But this does not get us all the way to deterministic
finalization. In order to reach that, we need the additional
support of local reference objects. (This has no analogous
support within Managed Extensions, and so it is not a
translation issue.)
Declaring
a Reference Object
Visual C++ 2008 supports the declaration of an object of
a reference class on the local stack or as a member of a
class as if it were directly accessible. When combined with
the association of the destructor with the
Dispose method, the result is the
automated invocation of finalization semantics on reference
types.
First, we define our reference class such that object
creation functions as the acquisition of a resource through
its class constructor. Secondly, within the class
destructor, we release the resource acquired when the object
was created.
Copy Code
public ref class R {
public:
R() { /* acquire expensive resource */ }
~R() { /* release expensive resource */ }
// � everything else �
};
The object is declared locally by using the type name but
without the accompanying hat. All uses of the object, such
as invoking a method, are done through the member selection
dot (.) instead of arrow (->).
At the end of the block, the associated destructor,
transformed into Dispose, is
invoked automatically, as shown here:
Copy Code
void f() {
R r;
r.methodCall();
// r is automatically destructed here �
// that is, r.Dispose() is invoked
}
As with the using statement
within C#, this does not defy the underlying CLR constraint
that all reference types must be allocated on the CLR heap.
The underlying semantics remain unchanged. The user could
equivalently have written the following (and this is likely
the internal transformation performed by the compiler):
Copy Code
// equivalent implementation
// except that it should be in a try/finally clause
void f() {
R^ r = gcnew R;
r->methodCall();
delete r;
}
In effect, under the new syntax, destructors are again
paired with constructors as an automated acquisition/release
mechanism tied to a local object's lifetime.
Declaring
an Explicit Finalize
In the new syntax, as we've seen, the destructor is
synthesized into the Dispose
method. This means that when the destructor is not
explicitly invoked, the garbage collector, during
finalization, will not as before find an associated
Finalize method for the object. To
support both destruction and finalization, we have
introduced a special syntax for providing a finalizer. For
example:
Copy Code
public ref class R {
public:
!R() { Console::WriteLine( "I am the R::finalizer()!" ); }
};
The ! prefix is analogous to
tilde (~) that introduces a
class destructor � that is, both post-lifetime methods have
a token prefixing the name of the class. If the synthesized
Finalize method occurs within a
derived class, an invocation of the base class
Finalize method is inserted at its
end. If the destructor is explicitly invoked, the finalizer
is suppressed. Here is what the transformation might look
like:
Copy Code
// internal transformation under new syntax
public ref class R {
public:
void Finalize() {
Console::WriteLine( "I am the R::finalizer()!" );
}
};
Moving
from Managed Extensions for C++ to Visual C++ 2005
The runtime behavior of a Managed Extensions for C++
program is changed when it is compiled under Visual C++ 2008
whenever a reference class contains a non-trivial
destructor. The required translation algorithm is similar to
the following:
If a destructor is present, rewrite that to be the
class finalizer.
If a Dispose method is
present, rewrite that into the class destructor.
If a destructor is present but there is no
Dispose method, retain the
destructor while performing the first item.
In moving your code from Managed Extensions to the new
syntax, you might miss performing this transformation. If
the application depended in some way on the execution of
associated finalization methods, the behavior of the
application will silently differ from the one you intended.