Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

New Syntax C++ in .NET Version 2


February, 2005: New Syntax C++ in .NET Version 2

From generics to templates—and back again

Richard is the author of Programming with Managed Extensions for Microsoft Visual C++ .NET 2003 (Microsoft Press, 2003). He can be contacted at [email protected].


Where to Get Whidbey C++


In this final installment of my miniseries about the new version of C++ in Whidbey, I examine some of the new language features. First, I look at how generics are declared in C++ and how they relate to C++ templates. I then describe the new syntax to declare managed arrays, and finally look at some of the new operators used for casts, pointers, and accessing type objects.

Generics

Perhaps the biggest change in the runtime for Whidbey is the support for generics. Managed C++ is both a consumer and provider of generics. In addition, the new version of Managed C++ can also use C++ templates. Since generics are a .NET concept, Managed C++ can use generic classes and methods written in other languages. Indeed, the Framework library contains several generic classes; in particular, generic collection classes in the System.Collections.Generic namespace. The syntax for using generics is similar to templates. Listing One is a class that is based on a generic parameter of type T using the modifier generic<class T>. A class can be based on more than one generic parameter. If this is the case, then you provide a comma-separated list of these parameters in the generic<> modifier.

Once you have declared the generic parameters, you are free to use them in the class. In Listing One, the constructor takes a parameter of this type, and uses this to initialize the field t. The generic type is used without the handle syntax (^), both in the declaration of the parameter and when you use the generic type parameter in your code. In Listing One, three instances of this generic type are created—one provides int as the generic parameter, another provides String^, and the third ArrayList^. The code in the Test class is used to provide the implementations for these three constructed types, but the Test<> class is used directly for the two constructed types that take reference parameters. A new type is created for the constructed type that takes the value type. In spite of these details, the Test<> class still provides the functionality for all of the constructed types, whether the generic parameter is a value type or reference type. Since the generic class does not know what type will be used at runtime, you should use reference syntax when using fields or method parameters of the generic type. If the generic type argument is a value type, then the object created from that type argument will still be a value type, and the object will not be boxed, even though the instance is accessed through pointer syntax.

The compiler has no knowledge of the possible members that the object will have. Consequently, you can only access the Object members. You might be tempted to cast the object to another type; however, dynamic_cast<> won't work because objects of the generic parameter type are not declared with handle syntax and the type used in dynamic_cast<> must have handle syntax. Using any other type of cast is unsatisfactory because no runtime check is performed. Consequently, an invalid cast could occur producing an invalid handle. The solution is to get the compiler to do some more work, and this happens with constraints.

When you declare a generic type, you can also declare constraints on what the type parameter can be. For example, if your generic class should be based on a disposable type, you can make this a constraint; see Listing Two. In this code, the constraint T : IDisposable is applied, which means that the type that can be used for the generic type parameter must implement IDisposable. If the type does not implement this interface, then the compiler issues an error. If the type does obey the constraint, then the code within the generic type can use the type as if it is the type in the constraint. So, in Listing Two, I do not have to cast the t field to IDisposable because the constraint is satisfied: The code can call the IDisposable methods directly. Constraints can have a list of one or more interfaces, and you can have a constraint that the type argument has a specific class as a base class. If you provide a base class constraint, it should be an additional constraint to the interface constraints.

Managed C++ also supports generic methods. All the concepts I have described for classes are applicable to methods. So you can declare a generic method with the generic keyword and a list of the type parameters; the method can then use the type for its parameters. A generic method can also have constraints; the code in the method can assume that the type satisfies the constraints, so you do not need to cast to the type parameter constraint. Listing Three is an example of this. The class Printer has a method called PrintDoc that must take a type that derives from IPrintable.

Again, there is a difference between constructed generic types with value types as the type parameter and those with reference types as the type parameter. If the generic is given a value type, the runtime creates a specialized type at runtime for each value type used for the generic type parameter. This is similar in some ways to templates, except that the type is created at runtime, not compile time. The reason why multiple types are created is because instances of value types can be of different sizes, so if a constructed type has a field that is a value type, the runtime must be able to allocate enough memory for the field. If you pass a reference type as the generic type parameter, then regardless of the actual type, the runtime always creates an instance of a generic type. For example, in Listing One, the Test<String^> and Test<ArrayList^> instances will both be created from the Test<T> class. The reason is that all reference types are accessed through reference handles, which always have the same size, so no changes need to be made to Test<T>.

Generics and Templates

So where do templates fit into this? Well, Managed C++ supported templates from the first version, but only on native types. These native types would be compiled to IL because the compiler would essentially expand the template and generate a value type for the instantiated type. In Whidbey, you can apply a C++ template to a managed class with the same meaning as templates in native code—the compiler generates an instantiated type at compile time from the type parameters that you provide.

This is the major difference between generics and templates: Generics are constructed at runtime from the generic type and the type arguments, whereas templates are instantiated at compile time to generate a new type. Further, a generic type can exist in a different assembly to the constructed type, whereas a managed template class cannot be exported from an assembly; therefore, the managed template class must be in the same assembly as the managed instantiated type.

Generics are not as flexible as C++ templates and do not support specialization or partial specialization (that is, a specialization for a specific type or a subset of types). Generics do not allow the type parameter to be used as the base class to the generic and you cannot provide default parameters to the generic type. Further, type parameters of generics can be constructed generic types, but not generic types. Templates applied to a managed class can do all of these things.

Arrays

The way that arrays are declared has changed in Whidbey. In earlier versions of Managed C++, you used a syntax similar to native C++ to declare and allocate an array. In Whidbey, this syntax is only used for native arrays of native types; managed arrays are of the type array<>. For example, the old syntax to allocate an array looks like this:

String __gc* a[] = new String __gc*[4];

The new syntax uses array<> in the stdcli::language namespace:

array<String^>^ a = gcnew array<String^>(4);

Using array<> makes the code more readable, especially when you consider how arrays are returned from functions. Listing Four shows the old syntax and new syntax for returning an array from a method. While syntactically correct, the old style appears confusing, and if the method has a long parameter list, it is easy to misread the method header and expect it to return a string rather than an array of strings. The new syntax is clearer and easier to read.

The syntax for declaring multidimensional arrays is straightforward—there is a version of array<> that takes the array type and the number of dimensions as parameters:

// Two dimensional 10 x 10 array
array<String^, 2>^ a2 = gcnew array<String^,
2>(10, 10);

Before leaving arrays, it is worth pointing out that the language now supports functions with a variable number of parameters, similar to how C# allowed this to happen with the params keyword. Declaring the method is straightforward: The last parameter of the method should be an array parameter and marked with the [ParamArray] attribute. The method gets access to the method parameters through this array. The real difference comes in using the method. In previous versions of Managed C++, you would have to create the array in the calling code to pass to the method. In the new version of the language, the compiler generates the array for you. Listing Five illustrates this. As you can see, the average method is called with a list of integers, and the compiler generates the array<int> to be passed to the method.

Casts

Since the first version, Managed C++ has supported the C++ cast operators dynamic_cast<>, static_cast<>, const_cast<>, and reinterpret_cast<>. dynamic_cast<> performs a runtime type check, and if the type cannot be cast to the requested type, the return will be a null value (in Whidbey, this is nullptr); this is usually used when you are not sure about the type being cast. Therefore, any code that has a dynamic_cast<> is accompanied with a check for a null value. static_cast<> converts without a type check and is usually used when you know that the type cast will succeed. Managed types used as parameters or variables can be marked with const, which will add a Microsoft.VisualC.IsConstModifier modifier to the parameter or variable. This modifier makes sense only to C++, but it is nevertheless useful in your code because it gets the compiler to perform checks on how parameters are used. The const_cast<> operator has the same function as it does in native C++: It is used to remove the constness of a variable. Finally, reinterpret_cast<> has the same meaning as it does in native C++: It is used to cast between two unrelated types. In general, you will not use reinterpret_cast<> with managed types because .NET has strong typing.

Earlier versions of Managed C++ provided the cast operator __try_cast<>. This was intended to be used only in debug builds in the places where you would expect to use static_cast<>. This cast operator would throw an exception if the cast failed, letting your testing determine that the static_cast<> was inappropriate so you could make adjustments to your code. In release builds, you would replace calls to __try_cast<> with calls to static_cast<>.

The __try_cast<> operator is no longer available, replaced with the safe_cast<> operator. However, this new operator has an important difference. The operator is guaranteed to generate verifiable IL, and this is important because static_cast<> will not produce verifiable IL. If you want to create safe code (/clr:safe), you should use the safe_cast<> operator. Unlike static_cast<>, this operator also performs a runtime check on a type and throws an InvalidCastException if the types are unrelated.

Another use of safe_cast<> is to explicitly box value types. The new version of C++ implicitly boxes value types if you use a value type when an object is required, but the safe_cast<> operator can be used to explicitly perform the cast:

int i = 42;
int^ p = safe_cast<int^>(i);

Unboxing is always explicit and again, safe_cast<> is the operator to use. So following on the previous example:

int j = safe_cast<int>(p);

Interior Pointers

C++ is a flexible and powerful language and one way this power expresses itself is through interior pointers. As the name suggests, interior pointers give you access to memory within objects. An interior pointer is a pointer to memory within the managed heap and lets you manipulate this memory directly. Interior pointers are clearly dangerous, so any code that uses interior pointers is not verifiable.

To declare an interior pointer, you use the interior_ptr<> operator and to initialize it, you use the & operator on a field member of an object. One use of interior pointers is to gain access to the memory allocated for an array. Once you have an interior pointer, you can perform pointer arithmetic on it like a native pointer and dereference the pointer to access the data that it points to. In effect, this breaks the encapsulation of managed objects. Listing Six illustrates a straightforward use for an interior pointer. The code creates an array of 10 integers, then obtains an interior pointer to the first item in the array. In the for loop, the pointer is dereferenced to get access to the memory within the array, and this is initialized with the index variable in the loop. With each iteration, the pointer is incremented so that it points to the next item in the array.

Another example of interior pointers involves managed strings. The vcclr.h header file contains a function called PtrToStringChars that is used to get access to the array of Char in the managed string. This function returns an interior_ptr<const Char>. The reason that the type is const is because strings are immutable; therefore, you should not change the string. Further, the character array in a string is fixed in size and so you must keep within these bounds to prevent your code from corrupting the string. However, with this knowledge in mind and with judicial use of const_cast<>, Listing Seven shows how to change a character in a string using an interior pointer.

As an example of the danger of interior pointers, take a look at Listing Eight. Here, I define a reference type that has two 64-bit integers and a string as fields. The KillMe method obtains an interior pointer on the x field and uses this to change the fields in the object by assignment through the dereferenced interior pointer. The interior pointer is then incremented to give access to the y member, which changes and is then incremented again so that it points to the s member. However, since the type of the interior pointer is __int64, it means that the pointer will attempt to write a 64-bit integer over the handle of the string member (and since the handle will be 32 bits on a 32-bit system, presumably this action will overwrite some other value on the managed heap). The consequence of this last action will be to corrupt the managed heap, so that when the string member is accessed in the Dump method, an exception is thrown. This exception cannot be caught, the managed heap is corrupted, and so the process is forcibly shut down. Be wary of interior pointers!

Pinning Pointers

You should not write code like Listing Seven. Managed strings are immutable; hence, you should not change a string like this. The real reason for PtrToStringChars is to give access to the Unicode array in the string so that you can pass it to unmanaged functions as a const wchar_t* pointer. However, there is a problem with passing an interior pointer to native code. During the time that the native code is executing, the garbage collection (GC) could perform a collection and compact the managed heap. If this occurs, the string object could be moved and so the interior pointer becomes invalid. This is not a problem if the interior pointer is used in managed code because the GC tracks the use of interior pointers and changes the pointer value accordingly. However, the GC cannot track the use of interior pointers passed to native code. To get around this issue, the pointer should be pinned. When you pin an interior pointer, the runtime will pin the entire object; pinning tells the GC that the object should not be moved in memory. To perform pinning, you create a pinning pointer with pin_ptr<>. For example:

pin_ptr<wchar_t> pinchars =
PtrToStringChars(s);
_putws(pinchars);

Here, pin_ptr<> pins the string, and the interior pointer to this pinned object is passed to the CRT function _putws, which prints out the value of the string. The object is pinned for the lifetime of the pinning pointer. In IL, variables are declared at method scope, so the pinning pointer will be valid for the entire lifetime of the method. However, you can get around this problem by assigning the pinning pointer to nullptr after you have finished using it, which indicates that the pinning pointer is no longer being used, so the object can be moved by the GC.

Type Objects

Each type has a static object called the "type" object, which is obtained by calling the Object::GetType method on an object. If you don't have an object, then you have to use some other mechanism to get a type object. In earlier versions of Managed C++, this was achieved with the __typeof operator. In Whidbey, this operator has been renamed typeid<>, the type is passed as the parameter to the operator, which then returns the type object. Incidentally, generic types also have type objects, as do constructed types. For example this code:

Console::WriteLine
(typeid< Dictionary<int,int> >);

prints this at the command line:

System.Collections.Generic.Dictionary`2
[System.Int32,System.Int32]

In other words, the Dictionary type has two type parameters (indicated by `2), and the constructed type has provided Int32 for both of these parameters. In the current beta, it is not possible to get the type object for a generic type: You must provide type parameters.

DDJ



Listing One

generic <class T>
ref class Test
{
   T t;
public:
   Test(T a)
   {
      t = a;
   }
   String^ ToString() override
   {
      return String::Format("value is {0}", t->ToString());
   }
};
void main()
{
   Test<int>^ t1 = gcnew Test<int>(10);
   Console::WriteLine(t1);
   Test<String^>^ t2 = gcnew Test<String^>("hello");
   Console::WriteLine(t2); 
   Test<ArrayList^>^ t3 = gcnew Test<ArrayList^>(gcnew ArrayList);
   Console::WriteLine(t3); 
}
Back to article


Listing Two
generic<class T> where T : IDisposable
ref class DisposableType
{
   T t;
public:
   DisposableType(T param) {t=param;}
   void Close()
   {
      t->Dispose();
   }
};
Back to article


Listing Three
interface class IPrintable
{
   void Print();
};
ref class Printer
{
public:
   generic<class T> where T : IPrintable
   void PrintDoc(T doc)
   {
      doc->Print();
   }
};
Back to article


Listing Four
// Old Syntax
String* Create(int i) __gc[]
{
   return new String __gc*[i];
}
// New Syntax
array<String^>^ create(int i)
{
   return gcnew array<String^>(i);
}
Back to article


Listing Five
using namespace System;
using namespace stdcli::language;

double average([ParamArray] array<int>^ arr) 
{
   double av = 0.0;
   for (int j = 0 ; j < arr->Length ; j++)
      av += arr[j];
   return av / arr->Length;
}
void main() 
{
   Console::WriteLine("average is {0}", average(9, 3, 4, 2));
}
Back to article


Listing Six
const int SIZE = 10;
array<int>^ a = gcnew array<int>(SIZE);
interior_ptr<int> p = &a[0];
for (int i = 0; i < SIZE; ++I, ++p)
   *p = i;
Back to article


Listing Seven
String^ s = "hello";
interior_ptr<const Char> ptr = PtrToStringChars(s);
interior_ptr<Char> p = const_cast< interior_ptr<Char> >(ptr);
Console::WriteLine(s); // prints hello
*p = 'H';
Console::WriteLine(s); // prints Hello
Back to article


Listing Eight
// Illustrating the danger in interior_ptr<>
using namespace System;
using namespace stdcli::language;

ref class Bad
{
   __int64 x;
   __int64 y;
   String^ s;
public:
   Bad()
   {
      x = 1LL; y = 2LL; s = "test";
   }
   void KillMe()
   {
      Dump();
      interior_ptr<__int64> p = &x;
      *p = 3LL; // Change x
      Dump();
      p++;
      *p = 4LL; // Change y
      Dump();
      p++;
      *p = 5LL; // Change s
      // Dump will kill the process when it tries to access s 
      Dump();
   }
   void Dump()
   {
      Console::WriteLine("{0} {1} {2}", x, y, s);
   }
};
void main()
{
   Bad^ bad = gcnew Bad;
   bad->KillMe();
}
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.