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 |
ref class RefClass1
{
void Func(){}
};
ref struct RefClass2
{
void Func(){}
};
|
Value types |
value class ValClass1
{
void Func(){}
};
value struct ValClass2
{
void Func(){}
};
|
Interface types |
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
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
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
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:
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:
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:
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
sealed
class can't be used as a base class for any other class—it seals
the class from further derivation:
ref class S sealed
{
};
ref class D : S { };
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
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
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:
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
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
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:
ref class Base
{
};
ref class Derived : Base {
};
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:
interface class IBase
{
};
ref class Derived1 : private IBase {}; ref class Derived2 : protected IBase {}; ref class Derived3 : IBase {};
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:
ref class RefBase {};
value class ValBase {};
interface class IBase {};
value class Derived1 : RefBase {}; value class Derived2 : ValBase {}; 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:
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
Student
object, and
SelectSubject
invokes a method on the
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:
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
ChangeString
to:
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#
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:
Student^ s1 = gcnew Student();
Student% s2 = *s1; Student^ s3 = %s2;
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.