< C++ .Net Early Stages 8 | Main | C++ .Net Early Stages 10 >


 

 

Early Stages of the C++ .Net 9

(Managed Extensions for C++)

 

 

Note: The code snippets used in this module are for Visual C++ .Net 2003 and if compiled using Visual C++ .Net 2005 you need to use the /clr:oldSyntax option). Here you will learn new keywords used in the C++ .Net though in VC++ 2005 these keywords already deprecated. However we just want to see how the whole thing changed to the managed C++ .Net from native, unmanaged C++ used in COM, MFC etc.

  1. Managed Arrays

 

 

 

 

 

Managed Arrays

 

In .NET, arrays are managed types; each array is an object and is allocated on the managed heap. The syntax to declare a managed array is slightly different from the syntax for declaring native arrays, and similarly the syntax to access the array, and to use the array as parameters for methods or return types from methods, is different than that of native arrays. If you come from a C++ background, you have to be careful with managed arrays because a native array variable is essentially a pointer into memory and the square brackets are used to perform memory arithmetic and dereference the pointer. With managed code, you should not normally access memory directly, and the .NET Framework classes actively prevent this access. This restriction means that some of the tricks that you are used to performing with native arrays and pointers you will not be able to perform with managed arrays. However, although some of these tricks are useful (for example, using negative indexes), the extra checks performed by the runtime mean that you gain enormously by having the safety of index validation and garbage collection that ensures that your code does not leak memory. Declaring a one-dimensional array is straightforward:

// arrays

String* names[] = __gc new String*[3];

names[0] = S"Richard";

names[1] = S"Thomas";

names[2] = S"Grimes";

The first line allocates an array of String* pointers. The type of this variable is String*[], which is not the same as String** (which is the type you would use to return a String reference as an in/out parameter). You can test the type of the array with this code:

Type* t = names->GetType();

while (t != 0)

{

   Console::WriteLine(t->ToString());

   t = t->BaseType;

}

The output actually shows the type of the variable as System.String[], which is the IL format that the runtime uses. This code also shows that the base class of the variable is System::Array. Thus, an array can be accessed through a typed array variable (String[]), a pointer to Array, or to Object, so all three of these declarations are allowed:

 

String* a1[] = names;

Array* a2 = names;

Object* a3 = names;

Of course, when you have an Array* pointer, you do not get the advantage of using the square bracket syntax, but it is possible to cast between the types. The Array class is abstract, so if you want to create an instance, you use the static method CreateInstance, which allows you to identify the type of the Array you want created. Our string array could be created like this:

Array* a = Array::CreateInstance(__typeof(String), 3);

String* names2[] = dynamic_cast<String*[]>(a);

names2[0] = S"Richard";

names2[1] = S"Thomas";

names2[2] = S"Grimes";

CreateInstance creates an array of the specified type, and the __typeof operator returns the static Type object for the type (the same object that is returned from GetType when called on an instance). When the array is created, the items are assigned to zero (for primitive types and __gc types) or the default constructor is called (for __value types), so you do not have to worry about the random values that occur in native C++. The runtime does not create the objects in the array when you declare it, so you have to allocate each item, as shown in the preceding code. The index of the array is always zero-based. If you use an index outside of the range 0 <= index < names->Length, where the Length property is one less than the value given in the declaration, a run-time exception of IndexOutOfRangeException will be thrown.

In this example, we have called the overloaded version of CreateInstance that creates a single-dimension array, a vector. There are overloads that can create multidimensional arrays and can specify the lower bounds of the array, for example:

int dims __gc[] = {3};

int idx __gc[] = {1};

Array* a = Array::CreateInstance(__typeof(String), dims, idx);

Here we have declared two int arrays, and to indicate that we want a managed array rather than a native array, we have to explicitly mention that the array is a __gc array. The dims array is used to specify the size of each dimension, and because this declaration has a single value, the array that we want created should have a single dimension of three items. The idx array gives the lower bound of each dimension. Here we have decided that the index of the first dimension should be 1-based (like default arrays in Visual Basic 6). This code will compile, and it will work at run time. However, if we attempt to use dynamic_cast<> to cast the Array* variable to a String*[] variable, we will get a zero pointer. We can use static_cast<> and the cast will succeed, but when we try to access indexes out of the range 0 through 2, we get an IndexOutOfRangeException exception:

String* names3[] = dynamic_cast<String*[]>(a);

 

names3[1] = S"Richard";

names3[2] = S"Thomas";

names3[3] = S"Grimes";

The reason is that the runtime sees that we want to use square brackets to access the array, and when we use dynamic_cast<>, the runtime check determines that the indexes of the dimensions make them incompatible with square bracket syntax. When we decide that we know better than the runtime by using static_cast<>, the cast succeeds, but whenever we access an item, the runtime performs an index bounds check, and it always assumes that the lower bound is zero. If you want to access an array with a lower bound other than zero, you have to use the methods on the Array class: GetValue and SetValue. In the previous example, we used an initializer list for the dims and idx arrays. You can create arrays of value types and __gc types with an initializer list, for example, using this __gc class:

__gc struct Person

{

   unsigned short age;

   String* name;

   Person(String* n, unsigned short a) : name(n),age(a){}

};

We can create the following arrays:

String* names[] = { S"Richard", S"Ellinor" };

Person* people[] = { new Person(S"Richard", 37), new Person(S"Ellinor", 38) };

You still have to allocate the members of the array using __gc new. String* arrays are an exception because when you give a string literal, the compiler will generate the code to use it to initialize a managed string. As indicated earlier, managed arrays can contain value types and pointers to __gc types. An array of value types will actually contain the values. You will get a contiguous buffer of memory containing the items. If you obtain an interior pointer to this buffer, you can access the items through pointer arithmetic.

// System::Char array can be initialized with characters or

// with unsigned short values. In this example, I ensure that the last item is zero.

Char c __gc [] = {'R', 0x0069, 0x0063, 'h', 'a', 'r', 'd', 0};

Char __gc* p = &c[0];

for (int i = 0; i < c->Length-1; i++, p++)

   Console::Write(__box(*p));

The second line gets an interior pointer to the first item in the array, and then, in the for loop, we dereference the pointer to get the item and then increment the pointer with each loop. This code, naturally, prints Richard at the console. If you want to pass this array to unmanaged code, you have to pin the array first. If you pin a single item in an array, the entire array is pinned.

Char __pin* p2 = &c[0];

// Pass this to _putws; this is the reason why the last item is zero.

_putws(static_cast<wchar_t*>(p2));

The syntax for allocating and accessing a one-dimensional array is not too different than the syntax for unmanaged arrays. The syntax to allocate and access multidimensional arrays looks quite different to native C++.

// multiarray.cpp

String* books[,] = new String*[3,2];

 

books[0,0] = S"Professional ATL COM Programming";

books[0,1] = S"1-861001-40-1";

books[1,0] = S"Professional Visual C++ 6 MTS Programming";

books[1,1] = S"1-861002-39-1";

books[2,0] = S"Developing Applications with Visual Studio .NET";

books[2,1] = S"0-201-70852-3";

This code creates a String array with two columns and three rows. C++ does not support initializer lists for multidimensional arrays, so we have to initialize each item individually. Note that the array is a rectangle that is, there are six elements arranged as two columns and three rows. In native C++, you can interchange pointer and square bracket syntax. a[n] is equivalent to *(a+n), so you can treat an array of three rows and two columns as an array of three rows where each item is a single-dimensional array with two items. Furthermore, arrays are allocated from contiguous memory, so you can play tricks with indexes (for example, for an array int[4][2], item [2][2] is the same as item [3][0]). When you use the square bracket syntax with managed arrays, you are restricted to the dimensions that you used when you declared the array. If you use an index out of this range, you’ll get an exception at run time. C++ does not allow you to cast between an Array* and a multidimensional array in square bracket syntax, so you cannot allocate using CreateInstance and then have the convenience of accessing the elements with the square bracket syntax. The memory for a multidimensional managed array is allocated as a contiguous block of memory, and you can take advantage of this layout with interior pointers. For example, we can allocate an array of integers like this:

// Create a multidimensional array.

int i __gc[,] = new int __gc[5,5];

for (int j=0; j<=i->GetUpperBound(0); j++)

{

   for (int k=0; k<=i->GetUpperBound(1); k++)

   {

      // Initialize the elements.

      i[j,k] = (j*10) + k;

   }

}

Here we use the inherited GetUpperBound to get the highest value of the index of the specified dimension, which for an array allocated using the square bracket syntax is one less than the size of the dimension given in the array declaration. We can then obtain an interior pointer to the array like this:

// Get an interior pointer to the first element.

int __gc* p = &i[0,0];

// Obtain all the elements.

for (int j = 0; j < (i->GetUpperBound(0) + 1)*(i->GetUpperBound(1) + 1); j++, p++)

{

   Console::WriteLine(S"{0}={1}", __box(j), __box(*p));

}

This code will print out all the items in the array without an index check. If we miscalculate the size of the array, we will obtain memory that does not belong to the array. As mentioned earlier, you must be careful when you use interior pointers. Because you could have free access to the managed heap, such code is not verifiable. When you have an array, you can call any of the inherited members from System::Array. Array implements IEnumerable, so you can call GetEnumerator to return an IEnumerator interface.

IEnumerator* e = i->GetEnumerator();

while (e->MoveNext())

{

   Console::WriteLine(e->Current->ToString());

}

The class has various methods that allow you to search for items in a one-dimensional array. The static methods IndexOf and LastIndexOf take the array and an object. IndexOf returns the index of the first element that matches the object, and LastIndexOf returns the index of the last element that matches the object passed to the method. You can also pass the range of indexes to search. The method calls Equals on the items in the array, which is usually implemented as a bitwise test for value types and a test of identity equality (the references are to the same physical object) for __gc types. These methods return the index of the first item that is found (the array can contain several references to the same object) or GetLowerBound(0)-1. (Usually the lower bound is zero, but it is better to explicitly check the value because in the future C++ might support arrays with lower bounds that are not zero). Now consider this code:

// checkcmd.cpp

__gc struct Item

{

   int i;

   Item(int j) : i(j){}

   // Other methods omitted

};

 

void main()

{

   String* args[] = Environment::GetCommandLineArgs();

   Item* items[] = new Item*[args->Length - 1];

   for (int j = args->GetLowerBound(0) + 1; j <= args->GetUpperBound(0); j++)

   {

      items[j-1] = new Item(Int32::Parse(args[j]));

   }

 

   int i = Array::IndexOf(items, new Item(42));

   if (i < args->GetLowerBound(0))

      Console::WriteLine(S"42 must be on the command line!");

   // Other code...

}

The intention is to take the parameters passed to the command line and use each one to create an Item object. The code needs to have at least one item initialized with the value of 42. As it stands, this code will always print the error message regardless of the items put on the command line. The reason is that the search is performed by passing a new instance of Item initialized to the value of 42 to IndexOf. The default implementation of Object::Equals will check for object identity, so even if a user types 42 at the command line, the object created from that command line argument will always be a different object from the one passed to Equals.

If Item were a __value type, this problem would not occur because the default implementation of Equals for a __value type is a bitwise comparison. If we performed the same search on the String* array (args), looking for the string “42”, the problem would not occur because String::Equals does a string comparison. We could implement Item::Equals to perform a comparison of the fields of the objects, but we might have other code that relies on Equals to be an identity check. The solution is to implement IComparable and to use Array::BinarySearch.

// checkcmd.cpp

__gc struct Item : IComparable

{

   int i;

   Item(int j) : i(j){}

   int CompareTo(Object* obj)

   {

      Item* item = dynamic_cast<Item*>(obj);

      if (i == item->i) return 0;

         if (i > item->i) return 1;

           return -1;

   }

};

BinarySearch will go through each item in the array and check for the IComparable interface on each. (If none of the items implement this interface, an exception is thrown.) BinarySearch then calls CompareTo on each item, passing the object with which to make the comparison. If the return value is zero, the objects are considered to have the same value. The change to the call to search the array looks like this:

int i = Array::BinarySearch(items, new Item(42));

You can also sort one-dimensional arrays with the Sort and Reverse static methods. Reverse takes an existing array and reverses the order of items in the array or within a range in the array. Sort is more interesting, it uses a quick sort algorithm to sort the items in the array or items within a range of the array. The items should implement IComparable, in which case CompareTo will be called on each item to perform the sort; or if the items do not implement this interface (or the implementation is not suitable), a separate comparer object (that implements IComparer) can be used:

// checkcmd.cpp

__gc struct ItemComparer : IComparer

{

   int Compare(Object* x, Object* y)

   {

      Item* item1 = dynamic_cast<Item*>(x);

      Item* item2 = dynamic_cast<Item*>(y);

      if (item1->i == item2->i) return 0;

      if (item1->i > item2->i) return 1;

      return -1;

   }

};

IComparer::Compare should return zero if the parameters are equal (using whatever criteria the comparer decides equals means; in this case, the state held by the objects). If the first object is greater than the second object, IComparer::Compare returns a number greater than zero, and if the first object is less than the second object, it returns a number less than zero. The array can be sorted like this:

Array::Sort(items, new ItemComparer);

for (int k = items->GetLowerBound(0); k <= items->GetUpperBound(0); k++)

{

   Console::WriteLine(S"item {0} = {1}", __box(k), __box(items[k]->i));

}

You can pass arrays as method parameters, either as an Array* pointer, as a pointer to one of the interfaces that it implements (IList, IEnumerable), or as a typed array. Passing an IList or IEnumerable pointer allows you to write generic routines that can be used with a whole range of containers. Declaring the method parameter as a typed array means that you can use the square bracket syntax in the method.

// arrayparams.cpp

void TimesTwo(int i __gc [])

{

   for (int j = i->GetLowerBound(0); j <= i->GetUpperBound(0); j++)

      i[j] *= 2;

}

Note that it is always a good idea to test the array size in the method. Methods can also return arrays using the standard C++ syntax, but the 2003 version of the Visual C++ compiler allows only this syntax for managed code. (Unmanaged code must use pointers.)

// arrayparams.cpp

int CreateArray(int size) __gc[]

{

   int i __gc[] = new int __gc [size];

   for (int j = i->GetLowerBound(0); j <= i->GetUpperBound(0); j++)

      i[j] = j;

   return i;

}

The .NET Framework class library has an attribute named [ParamArray], which is used on an array parameter to indicate that languages can treat the method as having a variable number of parameters, similar to the ... syntax in unmanaged C++. One overload of the Console::WriteLine uses this attribute.

[ParamArray] static void WriteLine(String* format, Object* arg __gc[]);

If this method is called in C#, the language allows any number of parameters to be placed after the format string and the language will generate an object array to pass to the method. You can write a similar method in C++ using [Param­Array], but note that if you call this method in C++, you will have to construct the array in your code. You might wonder how you can call WriteLine in C++ and pass more than four parameters. In this case, C++ will call another method that has a method signature that looks like this in IL:

.method public hidebysig static vararg void WriteLine(string format, object arg0, object arg1, object arg2, object arg3) cil managed;

This method truly does have a variable number of arguments because it has the varargs metadata attribute; in the implementation, the method obtains the parameters through the System::ArgIterator class. Clearly, C++ can call methods with the varargs attribute, but at present the only way to write such a method is if you write it in IL. C# neither has the facility to write, nor attempts to call, varargs methods.

 

 

 

 

Part 1 | Part 2 | Part 3 | Part 4 | Part 5 | Part 6 | Part 7 | Part 8

 


 

< C++ .Net Early Stages 8 | Main | C++ .Net Early Stages 10 >