Declaration of a Managed Class Type: From Managed Extension of C++ to New C++ .Net Syntax
The way to declare a reference class type changed from Managed Extensions of C++ to Visual C++ 2005. 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 simply indicates the public (for struct) or private (for class) default access level of its members declared within an initial unlabeled portion 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 are in the Managed Extensions:
Under the new syntax these are equivalently declared as follows:
The choice of ref (for reference type) over gc (for garbage collected) is thought to better suggest the fundamental nature of the type.
Specifying the Class as Abstract
Under Managed Extensions, the keyword __abstract is placed 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 such as:
Under the new syntax, the abstract contextual keyword is specified following the class name and before either the class body, base class derivation list, or semicolon.
Of course, the semantic meaning is unchanged.
|
Specifying the Class as Sealed
Under Managed Extensions, the keyword __sealed is placed before the class keyword (either before or after __gc) to indicate that objects of the class cannot be inherited from:
public __gc __sealed class String {};
Under the new syntax, the sealed contextual keyword is specified 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 (as shown below) is implicitly derived from Object. The benefit of sealing a class is that it allows 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.
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:
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:
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 simply to override the default interpretation. Many users felt this was a bit severe on the compiler's part.
// Managed Extensions: error: defaults to private derivation
__gc class foo : bar{};
In the new syntax definition, the absence of an access keyword defaults to a public derivation in a CLR inheritance definition. Thus, the public access keyword is no longer required, but optional. While this does not require any modification of Managed Extensions for C++ code, it is listed here for completeness.
// New syntax: ok: defaults to public derivation
ref class foo : bar{};
Declaration of a CLR Reference Class Object
The syntax to declare and instantiate an object of a reference class type has also changed from Managed Extensions for C++ to Visual C++ 2005. In Managed Extensions, a reference class type object is declared 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:
public __gc class Form1 : public System::Windows::Forms::Form
{
private:
System::ComponentModel::Container __gc *components;
Button __gc *button1;
DataGrid __gc *myDataGrid;
DataSet __gc *myDataSet;
void PrintValues(Array* myArr)
{
System::Collections::IEnumerator* myEnumerator = myArr->GetEnumerator();
Array *localArray;
myArr->Copy(myArr, localArray, myArr->Length);
}
};
Under the new syntax, a reference class type object is declared using a new declarative token (^) referred to formally as a tracking handle and more informally as a hat. The tracking adjective underscores the idea that a reference type sits within the CLR heap, and can therefore transparently move locations during garbage collection heap compaction. A tracking handle is transparently updated during runtime. Two analogous concepts are:
The tracking reference (%), and
The interior pointer (interior_ptr<>).
There are two primary reasons to move the declarative syntax away from a reuse of the ISO-C++ pointer syntax:
The use of the pointer syntax did not allow overloaded operators to be directly applied to a reference object; rather, one had to call the operator through its internal name, such as rV1->op_Addition(rV2) rather than the more intuitive rV1+rV2.
There are a number of pointer operations, such as casting and pointer arithmetic, that are disallowed for objects stored on a garbage collected heap. We felt that the notion of a tracking handle better captures the nature of a CLR reference type.
The use of 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 above Managed Extensions text translated into the new syntax:
public ref class Form1: public System::Windows::Forms::Form
{
private:
System::ComponentModel::Container^ components;
Button^ button1;
DataGrid^ myDataGrid;
DataSet^ myDataSet;
void PrintValues(Array^ myArr)
{
System::Collections::IEnumerator^ myEnumerator = myArr->GetEnumerator();
Array ^localArray;
myArr->Copy(myArr, localArray, myArr->Length);
}
};
Dynamic Allocation of an Object on the CLR Heap
In Managed Extensions, the existence of two new expressions to allocate between the native and managed heap was largely transparent. In nearly all instances, the compiler is able by context to correctly determine whether the native or managed heap is intended. For example,
Button *button1 = new Button; // OK: managed heap
int *pi1 = new int; // OK: native heap
Int32 *pi2 = new Int32; // OK: managed heap
In cases in which the contextual heap allocation is not the intended instance, one 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 above three declarations look as follows in the new syntax:
Button^ button1 = gcnew Button; // OK: managed heap
int * pi1 = new int; // OK: native heap
Int32^ pi2 = gcnew Int32; // OK: managed heap
Here is the Managed Extensions initialization of the Form1 members declared in the previous section:
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.
void InitializeComponent()
{
components = gcnew System::ComponentModel::Container;
button1 = gcnew System::Windows::Forms::Button;
myDataGrid = gcnew DataGrid;
button1->Click += gcnew System::EventHandler(this, &Form1::button1_Click);
}
A Tracking Reference to No Object
In the new syntax, 0 no longer represents a null address but is simply treated as an integer, the same as 1, 10, or 100, and so we needed to introduce a special token to represent a null value for a tracking reference. For example, in Managed Extensions, we initialize a reference type to address no object as follows:
// 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 results in an implicit boxing of that value type. In the new syntax, both obj and obj2 are initialized to addressed boxed Int32 objects holding, respectively, the values 0 and 1. For example:
// causes the implicit boxing of both 0 and 1
Object ^ obj = 0;
Object ^ obj2 = 1;
Therefore, in order to allow the explicit initialization, assignment, and comparison of a tracking handle to null, we introduced a new keyword, nullptr. And so the correct revision of the original example looks as follows:
// 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:
__value struct Holder
{
Holder(Continuation* c, Sexpr* v)
{
cont = c;
value = v;
args = 0;
env = 0;
}
private:
Continuation* cont;
Sexpr * value;
Environment* env;
Sexpr * args __gc [];
};
where 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:
value struct Holder
{
Holder(Continuation^ c, Sexpr^ v)
{
cont = c;
value = v;
args = nullptr;
env = nullptr;
}
private:
Continuation^ cont;
Sexpr^ value;
Environment^ env;
array<Sexpr^>^ args;
};
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:
Sexpr * Loop (Sexpr* input)
{
value = 0;
Holder holder = Interpret(this, input, env);
while (holder.cont != 0)
{
if (holder.env != 0)
{
holder=Interpret(holder.cont,holder.value,holder.env);
}
else if (holder.args != 0)
{
holder = holder.value->closure()->apply(holder.cont,holder.args);
}
}
return value;
}
and here is the revision, turning each 0 instance into a nullptr. The translation tool helps in this transformation, automating many if not all of the occurrences, including use of the NULL macro.
Sexpr ^ Loop (Sexpr^ input)
{
value = nullptr;
Holder holder = Interpret(this, input, env);
while (holder.cont != nullptr)
{
if (holder.env != nullptr)
{
holder=Interpret(holder.cont,holder.value,holder.env);
}
else if (holder.args != nullptr)
{
holder = holder.value->closure()->apply(holder.cont,holder.args);
}
}
return value;
}
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 only legal as an initial value to the first two.
// 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:
void funct(Object^); // (1)
void funct(char*); // (2)
void funct(int); // (3)
An invocation with nullptr literal, such as the following,
// Error: ambiguous: matches (1) and (2)
funct(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 requires an explicit cast in order to disambiguate. An invocation with 0 exactly matches instance (3):
// OK: matches (3)
funct(0);
because a 0 is of type integer. Were funct(int) not present, the call would unambiguously match funct(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. This is why there is no ambiguity.