Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++/CLI

C++/CLI in Action - Declaring CLR types

4.95/5 (39 votes)
24 Feb 2007CPOL16 min read 1  
Excerpts from Chapter 1. Topics are (1) Declaring CLR types and (2) Handles: The CLI equivalent to pointers
Title C++/CLI in Action
Author Nishant Sivakumar
Publisher Manning
Published March 2007
ISBN-10 1-932394-81-8
ISBN-13 978-1932394818
Price USD 44.99

This is a chapter excerpt from C++/CLI in Action authored by Nishant Sivakumar and published by Manning Publications. The content has been reformatted for CodeProject and may differ in layout from the printed book and the e-book.

1.3 Declaring CLR types

In this section, we'll look at the syntax for declaring CLI (or CLR) types, modifiers that can be applied to CLI types, and how CLI types implement inheritance. C++/ CLI supports both native (unmanaged) and managed types and uses a consistent syntax for declaring various types. Native types are declared and used just as they are in standard C++. Declaring a CLI type is similar to declaring a native type, except that an adjective is prefixed to the class declaration that indicates the type being declared. Table 1.3 shows examples of CLI type declarations for various types.

CLI type Declaration syntax
Reference types
MC++
ref class RefClass1
{
    void Func(){}
};

ref struct RefClass2
{
    void Func(){}
};
Value types
MC++
value class ValClass1
{
    void Func(){}
};

value struct ValClass2
{
    void Func(){}
};
Interface types
MC++
interface class IType1
{
    void Func();
};

interface struct IType2
{
    void Func();
};
Table 1.3 Type declaration syntax for CLI types

C# developers may be a little confused by the usage of both

MC++
class
and struct for both reference and value types. In C++/CLI, struct and class can be used interchangeably (just as in standard C++), and they follow standard C++ visibility rules for structs and classes. In a class, methods are private by default; in a struct, methods are public by default. In table 1.3, RefClass1::Func and ValClass1::Func are both private, whereas
MC++
RefClass2::Func 
and ValClass2::Func are both public. For the sake of clarity and consistency with C#, you may want to exclusively use ref class for ref types and value struct for value types instead of mixing class and struct for both ref and value types.

Interface methods are always public; declaring an interface as a struct is equivalent to declaring it as a class. This means

MC++
IType1::Func
and IType2::Func are both public in the generated MSIL. C# developers must keep the following in mind:

  • A C++/CLI value class (or value struct) is the same as a C# struct.
  • A C++/CLI ref class (or ref struct) is the same as a C# class.

Those of you who have worked on the old MC++ syntax should remember these three points:

  • A ref class is the same as an __gc class.
  • A value class is the same as an __value class.
  • An interface class is the same as an __interface.

Spaced keywords

An interesting thing that you need to be aware of is that only three new, reserved keywords have been introduced in C++/CLI: gcnew, nullptr, and generic. All the other seemingly new keywords are spaced (or contextual) keywords. Syntactic phrases like ref class, for each, and value class are spaced keywords that are treated as single tokens in the compiler's lexical analyzer. The big advantage is that any existing code that uses these new keywords (like ref or each) continues to compile correctly, because it's not legal in C++ to use a space in an identifier. The following code is perfectly valid in C++/CLI:

MC++
int ref = 0;
int value = ref;
bool each = value == ref;

Of course, if your existing code uses gcnew, nullptr, or generic as an identifier, C++/CLI won't compile it, and you'll have to rename those identifiers.

You've seen how CLI types can be declared. Next, you'll see how type modifiers can be applied to these classes (or structs, as the case may be).

1.3.1 Class modifiers

You can specify the abstract and sealed modifiers on classes; a class can be marked both abstract and sealed. But such classes can't be derived explicitly from any base class and can only contain static members. Because global functions aren't CLS-compliant, you should use abstract sealed classes with static functions, instead of global functions, if you want your code to be CLS-compliant.

In case you're wondering when and why you would need to use these modifiers, remember that, to effectively write code targeting the .NET Framework, you should be able to implement every supported CLI paradigm. The CLI explicitly supports abstract classes, sealed classes, and classes that are both abstract and sealed. If the CLI supports it, you should be able to do so, too.

Just as with standard C++, an abstract class can only be used as a base class for other classes. It isn't required that the class contains abstract methods for it to be declared as an abstract class, which gives you extra flexibility when designing your class hierarchy. The following class is abstract because it's declared abstract, although it doesn't contain any abstract methods:

MC++
ref class R2 abstract
{
public:
    virtual void Func(){}
};

An interesting compiler behavior is that if you have a class with an abstract method that isn't marked abstract, such as the following class, the compiler issues warning C4570 (class is not explicitly declared as abstract but has abstract functions) instead of issuing an error:

MC++
ref class R1
{
public:
    virtual void Func() abstract;
};

In the generated IL, the class R1 is marked abstract, which means that if you try to instantiate the class, you'll get a compiler error (and you should). Not marking a class abstract when it has abstract methods is untidy, and I strongly encourage you to explicitly mark classes abstract if at least one of their methods is abstract. Note how I've used the abstract modifier on a class method in the previous example; you'll see more on this and other function modifiers in chapter 2.

Using the sealed modifier follows a similar syntax. A

MC++
sealed
class can't be used as a base class for any other class—it seals the class from further derivation:

MC++
ref class S sealed
{
};

ref class D : S // This won't compile
{               // Error C3246
};

Sealed classes are typically used when you don't want the characteristics of a specific class to be modified (through a derived class), because you want to ensure that all instances of that class behave in a fixed manner. Because a derived class can be used anywhere the base class can be used, if you allow your class to be inherited from, by using instances of the derived class where the base class instance is expected, users of your code can alter the expected functionality (which you want to remain unchangeable) of the class. For example, consider a banking application that has a CreditCardInfo class that is used to fetch information about an account holder's credit-card transactions. Because instances of this class will be occasionally transmitted across the Internet, all internal data is securely stored using a strong encryption algorithm. By allowing the class to be inherited from, there is the risk of an injudicious programmer forgetting to properly follow the data encryption implemented by the CreditCardInfo class; thus any instance of the derived class is inherently insecure. By marking the CreditCardInfo class as sealed, such a contingency can be easily avoided.

A performance benefit with using a sealed class is that, because the compiler knows a sealed class can't have any derived classes, it can statically resolve virtual member invocations on a sealed class instance using nonvirtual invocations. For example, assuming that the CreditCardInfo class overrides the GetHashCode method (which it inherits from

MC++
Object
), when you call GetHashCode at runtime, the CLR doesn't have to figure out which function to call. This is the case because it doesn't have to determine the polymorphic type of the class (because a
MC++
CreditCardInfo
object can only be a CreditCardInfo object, it can't be an object of a derived type—there are no derived types). It directly calls the GetHashCode method defined by the CreditCardInfo class.

Look at the following example of an abstract sealed class:

MC++
ref class SA abstract sealed
{
public:
    static void DoStuff(){}
private:
    static int bNumber = 0;
};

As mentioned earlier, abstract sealed classes can't have instance methods; attempting to include them will throw compiler error C4693. This isn't puzzling when you consider that an instance method on an

MC++
abstract sealed 
class would be worthless, because you can never have an instance of such a class. An abstract sealed class can't be explicitly derived from a base class, although it implicitly derives from
MC++
System::Object
. For those of you who've used C#, it may be interesting to know that an abstract sealed class is the same as a C# static class.

Now that we've discussed how to declare CLI types and apply modifiers on them, let's look at how CLI types work with inheritance.

1.3.2 CLI types and inheritance

Inheritance rules are similar to those in standard C++, but there are differences, and it's important to realize what they are when using C++/CLI. The good thing is that most of the differences are obvious and natural ones dictated by the nature of the CLI. Consequently, you won't find it particularly strenuous to remember them.

Reference types (ref class/struct) only support public inheritance, and if you skip the access keyword, public inheritance is assumed:

MC++
ref class Base
{
};

ref class Derived : Base // implicitly public
{
};

If you attempt to use private or protected inheritance, you'll get compiler error C3628. The same rule applies when you implement an interface; interfaces must be implemented using public inheritance, and if you skip the access keyword, public is assumed:

MC++
interface class IBase
{
};

ref class Derived1 : private IBase {}; //error C3141
ref class Derived2 : protected IBase {}; //error C3141
ref class Derived3 : IBase {}; //public assumed

The rules for value types and inheritance are slightly different from those for ref types. A value type can only implement interfaces; it can't inherit from another value or ref type. That's because value types are implicitly derived from System::ValueType. Because CLI types don't support multiple base classes, value types can't have any other base class. In addition, value types are always sealed and can't be used as base classes. In the following code snippet, only the Derived3 class compiles. The other two classes attempt to inherit from a ref class and a value class, neither of which is permitted:

MC++
ref class RefBase {};
value class ValBase {};
interface class IBase {};

value class Derived1 : RefBase {}; //error C3830
value class Derived2 : ValBase {}; //error C3830
value class Derived3 : IBase {};

These restrictions are placed on value types because value types are intended to be simple types without the complexities of inheritance or referential identity, which can be implemented using basic copy-by-value semantics. Also note that these restrictions are imposed by the CLI and not by the C++ compiler. The C++ compiler merely complies with the CLI rules for value types. As a developer, you need to keep these restrictions in mind when designing your types. Value types are kept simple to allow the CLR to optimize them at runtime where they're treated like simple plain old data (POD) types like an int or a char, thus making them extremely efficient compared to reference types.

Here's a simple rule you can follow when you want to decide whether a class should be a value type: Try to determine if you want it to be treated as a class or as plain data. If you want it to be treated as a class, don't make it a value type; but if you want it to behave just as an int or a char would, chances are good that your best option is to declare it as a value type. Typically, you'll want it to be treated as a class if you expect it to support virtual methods, user-defined constructors, and other aspects characteristic of a complex data type. On the other hand, if it's just a class or a struct with some data members that are themselves value types, such as an int or char, you may want to make that a value type.

One important point to be aware of is that CLI types don't support multiple inheritance. So, although a CLI type can implement any number of interfaces, it can have only one immediate parent type; if none is specified, this is implicitly assumed to be System::Object.

Next, we'll talk about one of the most important features that has been introduced in VC++ 2005: the concept of handles.

1.4 Handles: The CLI equivalent to pointers

Handles are a new concept introduced in C++/CLI; they replace the __gc pointer concept used in Managed C++. Earlier in the chapter, we discussed the pointer-usage confusion that prevailed in the old syntax. Handles solve that confusion. In my opinion, the concept of handles has contributed the most in escalating C++ as a first-class citizen of the .NET programming language world. In this section, we'll look at the syntax for using handles. We'll also cover the related topic of using tracking references.

1.4.1 Syntax for using handles

A handle is a reference to a managed object on the CLI heap and is represented by the ^ punctuator (pronounced hat).

NOTE When I say punctuator in this chapter, I'm talking from a compiler perspective. As far as the language syntax is concerned, you can replace the word punctuator with operator and retain the same meaning.

Handles are to the CLI heap what native pointers are to the native C++ heap; and just as you use pointers with heap-allocated native objects, you use handles with managed objects allocated on the CLI heap. Be aware that although native pointers need not always necessarily point to the native heap (you could get a native pointer pointing to the managed heap or to non-C++ allocated memory storage), managed handles have a close-knit relationship with the managed heap. The following code snippet shows how handles can be declared and used:

MC++
String^ str = "Hello world";
Student^ student = Class::GetStudent("Nish");
student->SelectSubject(150);

In the code, str is a handle to a System::String object on the CLI heap, student is a handle to a

MC++
Student
object, and SelectSubject invokes a method on the
MC++
student 
handle.

The memory address that str refers to isn't guaranteed to remain constant. The String object may be moved around after a garbage-collection cycle, but str will continue to be a reference to the same System::String object (unless it's programmatically changed). This ability of a handle to change its internal memory address when the object it has a reference to is moved around on the CLI heap is called tracking.

Handles may look deceitfully similar to pointers, but they are totally different entities when it comes to behavior. Table 1.4 illustrates the differences between handles and pointers.

Handles Pointers
Handles are denoted by the ^ punctuator. Pointers are denoted by the * punctuator.
Handles are references to managed objects on the CLI heap. Pointers point to memory addresses.
Handles may refer to different memory locations throughout their lifetime, depending on GC cycles and heap compactions. Pointers are stable, and garbage-collection cycles don't affect them.
Handles track objects, so if the object is moved around, the handle still has a reference to that object. If an object pointed to by a native pointer is programmatically moved around, the pointer isn't updated.
Handles are type-safe. Pointers weren't designed for type-safety.
The gcnew operator returns a handle to the instantiated CLI object. The new operator returns a pointer to the instantiated native object on the native heap.
It isn't mandatory to delete handles. The Garbage Collector eventually cleans up all orphaned managed objects. It's your responsibility to call delete on pointers to objects that you've allocated; if you don't do so, you'll suffer a memory leak.
Handles can't be converted to and from a void^. Pointers can convert to and from a void*.
Handles don't allow handle arithmetic. Pointer arithmetic is a popular mechanism to manipulate native data, especially arrays.
Table 1.4 Differences between handles and pointers

Despite all those differences, typically you'll find that for most purposes, you'll end up using handles much the same way you would use pointers. In fact, the * and -> operators are used to dereference a handle (just as with a pointer). But it's important to be aware of the differences between handles and pointers. The VC++ team members initially called them managed pointers, GC pointers, and tracking pointers. Eventually, the team decided to call them handles to avoid confusion with pointers; in my opinion, that was a smart decision.

Now that we've covered handles, it's time to introduce the associated concept of tracking references.

1.4.2 Tracking references

Just as standard C++ supports references (using the & punctuator) to complement pointers, C++/CLI supports tracking references that use the % punctuator to complement handles. The standard C++ reference obviously can't be used with a managed object on the CLR heap, because it's not guaranteed to remain in the same memory address for any period of time. The tracking reference had to be introduced; and, as the name suggests, it tracks a managed object on the CLR heap. Even if the object is moved around by the GC, the tracking reference will still hold a reference to it. Just as a native reference can bind to an l-value, a tracking reference can bind to a managed l-value. And interestingly, by virtue of the fact that an l-value implicitly converts to a managed l-value, a tracking reference can bind to native pointers and class types, too. Let's look at a function that accepts a String^ argument and then assigns a string to it. The first version doesn't work as expected; the calling code finds that the String object it passed to the function hasn't been changed:

MC++
void ChangeString(String^ str)
{
    str = "New string";
}

int main(array<System::String^>^ args)
{
    String^ str = "Old string";
    ChangeString(str);
    Console::WriteLine(str);
}

If you execute this code snippet, you'll see that str contains the old string after the call to ChangeString. Change

MC++
ChangeString
to:

MC++
void ChangeString(String^% str)
{
    str = "New string";
}

You'll now see that str does get changed, because the function takes a tracking reference to a handle to a String object instead of a String object, as in the previous case. A generic definition would be to say that for any type T, T% is a tracking reference to type T. C# developers may be interested to know that MSIL-wise, this is equivalent to passing the String as a C#

MC++
ref
argument to ChangeString. Therefore, whenever you want to pass a CLI handle to a function, and you expect the handle itself to be changed within the function, you need to pass a tracking reference to the handle to the function.

In standard C++, in addition to its use in denoting a reference, the & symbol is also used as a unary address-of operator. To keep things uniform, in C++/CLI, the unary % operator returns a handle to its operand, such that the type of %T is T^ (handle to type T). If you plan to use stack semantics (which we'll discuss in the next chapter), you'll find yourself applying the unary % operator quite a bit when you access the .NET Framework libraries. This is because the .NET libraries always expect a handle to an object (because C++ is the only language that supports a non-handle reference type); so, if you have an object declared using stack semantics, you can apply the unary % operator on it to get a handle type that you can pass to the library function. Here's some code showing how to use the unary % operator:

MC++
Student^ s1 = gcnew Student();
Student% s2 = *s1; // Dereference s1 and assign
                   // to the tracking reference s2
Student^ s3 = %s2; // Apply unary % on s2 to return a Student^

Be aware that the * punctuator is used to dereference both pointers and handles, although symmetrically thinking, a ^ punctuator should been used to dereference a handle. Perhaps this was designed this way to allow us to write agnostic template/generic classes that work on both native and unmanaged types.

You now know how to declare a CLI type; you also know how to use handles to a CLI type. To put these skills to use, you must understand how CLI types are instantiated, which is what we'll discuss in the next section.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)