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 pointerusage
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:
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.
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:
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
nonhandle 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; // 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.
1.5 INSTATIATING CLI CLASSES
In this section, you’ll see how CLI classes are instantiated using the gcnew operator.
You’ll also learn how constructors, copy constructors, and assignment operators
work with managed types. Although the basic concepts remain the same, the
nature of the CLI imposes some behavioral differences in the way constructors
and assignment operators work; when you start writing managed classes and
libraries, it’s important that you understand those differences. Don’t worry about
it, though. Once you’ve seen how managed objects work with constructors and
assignment operators, the differences between instantiating managed and native
objects will automatically become clear.
1.5.1 The gcnew operator
The gcnew operator is used to instantiate CLI objects. It returns a handle to the
newly created object on the CLR heap. Although it’s similar to the new operator,
there are some important differences: gcnew has neither an array form nor a
placement form, and it can’t be overloaded either globally or specifically to a class.
A placement form wouldn’t make a lot of sense for a CLI type, when you consider
that the memory is allocated by the Garbage Collector. It’s for the same reason
you aren’t permitted to overload the gcnew operator. There is no array form for
gcnew because CLI arrays use an entirely different syntax from native arrays,
which we’ll cover in detail in the next chapter. If the CLR can’t allocate enough
memory for creating the object, a System::OutOfMemoryException is thrown,
although chances are slim that you’ll ever run into that situation. (If you do get an
OutOfMemoryException, and your system isn’t running low on virtual memory,
it’s likely due to badly written code such as an infinite loop that keeps creating
objects that are erroneously kept alive.) The following code listing shows a typical
usage of the gcnew keyword to instantiate a managed object (in this case, the
Student object):
The gcnew operator is compiled into the newobj MSIL instruction by the C++/CLI
compiler. The newobj MSIL instruction creates a new CLI object—either a ref
object on the CLR heap or a value object on the stack—although the C++/CLI
compiler uses a different mechanism to handle the usage of the gcnew operator to
create value type objects (which I’ll describe later in this section). Because gcnew
in C++ translates to newobj in the MSIL, the behavior of gcnew is pretty much
dependent on, and therefore similar to, that of the newobj MSIL instruction. In
fact, newobj throws System::OutOfMemoryException when it can’t find enough
memory to allocate the requested object. Once the object has been allocated on
the CLR heap, the constructor is called on this object with zero or more arguments
(depending on the constructor overload that was used). On successful completion
of the call to the constructor, gcnew returns a handle to the instantiated
object. It’s important to note that if the constructor call doesn’t successfully complete,
as would be the case if an exception was raised inside the constructor, gcnew
won’t return a handle. This can be easily verified with the following code snippet:
ref class Student
{
public:
Student()
{
throw gcnew Exception("hello world");
}
};
//...
Student^ student = nullptr; //initialize the handle to nullptr
try
{
student = gcnew Student(); //attempt to create object
}
catch(Exception^)
{
}
if(student == nullptr) //check to see if student is still nullptr
Console::WriteLine("reference not allocated to handle");
Not surprisingly, student is still nullptr when it executes the if block. Because
the constructor didn’t complete executing, the CLR concludes that the object
hasn’t fully initialized, and it doesn’t push the handle reference on the stack (as it
would if the constructor had completed successfully).
NOTE: C++/CLI introduces the concept of a universal null literal called nullptr.
This lets you use the same literal (nullptr) to represent a null pointer
and a null handle value. The nullptr implicitly converts to a pointer or
handle type; for the pointer, it evaluates to 0, as dictated by standard
C++; for the handle, it evaluates to a null reference. You can use the
nullptr in relational, equality, and assignment expressions with both
pointers and handles.
As I mentioned earlier, using gcnew to instantiate a value type object generates
MSIL that is different from what is generated when you instantiate a ref type. For
example, consider the following code, which uses gcnew to instantiate a value type:
value class Marks
{
public:
int Math;
int Physics;
int Chemistry;
};
//...
Marks^ marks = gcnew Marks();
For this code, the C++/CLI compiler uses the initobj MSIL instruction to create a
Marks object on the stack. This object is then boxed to a Marks^ object. We’ll discuss
boxing and unboxing in the next section; for now, note that unless it’s imperative
to the context of your code to gcnew a value type object, doing so is
inefficient. A stack object has to be created, and this must be boxed to a reference
object. Not only do you end up creating two objects (one on the managed stack,
the other on the managed heap), but you also incur the cost of boxing. The more
efficient way to create an object of type Marks (or any value type) is to declare it on
the stack, as follows:
Marks marks;
You’ve seen how calling gcnew calls the constructor on the instance of the type
being created. In the coming section, we’ll take a more involved look at how constructors
work with CLI types.
If you have a ref class, and you haven’t written a default constructor, the compiler
generates one for you. In MSIL, the constructor is a specially-named instance
method called .ctor. The default constructor that is generated for you calls the
constructor of the immediate base class for the current class. If you haven’t specified
a base class, it calls the System::Object constructor, because every ref object
implicitly derives from System::Object. For example, consider the following two
classes, neither of which has a user-defined constructor:
ref class StudentBase
{
};
ref class Student: StudentBase
{
};
Neither Student nor StudentBase has a user-provided default constructor, but the
compiler generates constructors for them. You can use a tool such as ildasm.exe
(the IL Disassembler that comes with the .NET Framework) to examine the generated
MSIL. If you do that, you’ll observe that the generated constructor for Student
calls the constructor for the StudentBase object:
call instance void StudentBase::.ctor()
The generated constructor for StudentBase calls the System::Object constructor:
Just as with standard C++, if you have a constructor—either a default constructor
or one that takes one or more arguments—the compiler won’t generate a default
constructor for you. In addition to instance constructors, ref classes also support
static constructors (not available in standard C++). A static constructor, if
present, initializes the static members of a class. Static constructors can’t have
parameters, must also be private, and are automatically called by the CLR. In
MSIL, static constructors are represented by a specially named static method
called .cctor. One possible reason both special methods have a . in their names
is that this avoids name clashes, because none of the CLI languages allow a . in a
function name. If you have at least one static field in your class, the compiler generates
a default static constructor for you if you don’t include one on your own.
When you have a simple class, such as the following, the generated MSIL will have
a static constructor even though you haven’t specified one:
ref class StudentBase
{
static int number;
};
Due to the compiler-generated constructors and the implicit derivation from
System::Object, the generated class looks more like this:
ref class StudentBase : System::Object
{
static int number;
StudentBase() : System::Object()
{
}
static StudentBase()
{
}
};
A value type can’t declare a default constructor because the CLR can’t guarantee
that any default constructors on value types will be called appropriately, although
members are 0-initialized automatically by the CLR. In any case, a value type
should be a simple type that exhibits value semantics, and it shouldn’t need the
complexity of a default constructor—or even a destructor, for that matter. Note
that in addition to not allowing default constructors, value types can’t have userdefined
destructors, copy constructors, and copy-assignment operators.
Before you end up concluding that value types are useless, you need to think
of value types as the POD equivalents in the .NET world. Use value types just as
you’d use primitive types, such as ints and chars, and you should be OK. When
you need simple types, without the complexities of virtual functions, constructors
and operators, value types are the more efficient option, because they’re allocated
on the stack. Stack access will be faster than accessing an object from the garbagecollected
CLR heap. If you’re wondering why this is so, the stack implementation
is far simpler when compared to the CLR heap. When you consider that the CLR
heap also intrinsically supports a complex garbage-collection algorithm, it
becomes obvious that the stack object is more efficient.
It must be a tad confusing when I mention how value types behave differently
from reference types in certain situations. But as a developer, you should be able
to distinguish the conceptual differences between value types and reference
types, especially when you design complex class hierarchies. As we progress
through this book and see more examples, you should feel more comfortable with
these differences.
Because we’ve already talked about constructors, we’ll discuss copy constructors
next.
1.5.3 Copy constructors
A copy constructor is one that instantiates an object by creating a copy of another
object. The C++ compiler generates a copy constructor for your native classes,
even if you haven’t explicitly done so. This isn’t the case for managed classes.
Consider the following bit of code, which attempts to copy-construct a ref object:
ref class Student
{
};
int main(array<System::String^>^ args)
{
Student^ s1 = gcnew Student();
Student^ s2 = gcnew Student(s1);
}
If you run that through the compiler , you’ll get compiler error C3673 (class does
not have a copy-constructor). The reason for this error is that, unlike in standard
C++, the compiler won’t generate a default copy constructor for your class. At
least one reason is that all ref objects implicitly derive from System::Object,
which doesn’t have a copy constructor. Even if the compiler attempted to generate
a copy constructor for a ref type, it would fail, because it wouldn’t be able to
access the base class copy constructor (it doesn’t exist).
To make that clearer, think of a native C++ class Base with a private copy constructor,
and a derived class Derived (that publicly inherits from Base). Attempting
to copy-construct a Derived object will fail because the base class copy
constructor is inaccessible. To demonstrate, let’s write a class that is derived from
a base class that has a private copy constructor:
class Base
{
public:
Base(){}
private:
Base(const Base&);
};
class Derived : public Base
{
};
int _tmain(int argc, _TCHAR* argv[])
{
Derived d1;
Derived d2(d1); // <-- won't compile
}
Because the base object’s copy constructor is declared as private and therefore
is inaccessible from the derived object, this code won’t compile: The compiler is
unable to copy-construct the derived object. What happens with a ref class is similar
to this code. In addition, unlike native C++ objects, which aren’t polymorphic
unless you access them via a pointer, ref objects are implicitly polymorphic
(because they’re always accessed via reference handles to the CLR heap). This
means a compiler-generated copy constructor may not always do what you expect
it to do. When you consider that ref types may contain member ref types, there is
the question of whether a copy constructor implements shallow copy or deep
copy for those members. The VC++ team presumably decided that there were
too many equations to have the compiler automatically generate copy constructors
for classes that don’t define them.
If you want copy-construction support for your class, you must implement it
explicitly, which fortunately isn’t a difficult task. Let’s add a copy constructor to
the Student class:
That wasn’t all that tough, was it? Notice how you have to explicitly add a default
parameterless constructor to the class. This is because it won’t be generated by
the compiler when the compiler sees that there is another constructor present.
One limitation with this copy constructor is that the parameter has to be a
Student^, which is OK except that you may have a Student object that you want to
pass to the copy constructor. If you’re wondering how that’s possible, C++/CLI
supports stack semantics, which we’ll cover in detail in chapter 3. Assume that you
have a Student object s1 instead of a Student^, and you need to use that to invoke
a copy constructor:
As you can see, that code won’t compile. There are two ways to resolve the problem.
One way is to use the unary % operator on the s1 object to get a handle to the
Student object:
Student s1;
Student^ s2 = gcnew Student(%s1);
Although that compiles and solves the immediate problem, it isn’t a complete
solution when you consider that every caller of your code needs to do the same
thing if they have a Student object instead of a Student^. An alternate solution is
to have two overloads for the copy constructor, as shown in Listing 1.2.
This solves the issue of a caller requiring the right form of the object, but it brings
with it another problem: code duplication. You could wrap the common code in a
private method and have both overloads of the copy constructor call this
method, but then you couldn’t take advantage of initialization lists.
Eventually, it’s a design choice you have to make. If you only have the copy
constructor overload taking a Student^, then you need to use the unary % operator
when you have a Student object; and if you only have the overload taking a
Student%, then you need to dereference a Student^ using the * operator before
using it in copy construction. If you have both, you may end up with possible code
duplication; and the only way to avoid code duplication (using a common function
called by both overloads) deprives you of the ability to use initialization lists.
My recommendation is to use the overload that takes a handle (in the previous
example, the one that takes a Student^), because this overload is visible to other
CLI languages such as C# (unlike the other overload)—which is a good thing if
you ever run into language interop situations. The unary % operator won’t really
slow down your code; it’s just an extra character that you need to type. I also
suggest that you stay away from using two overloads, unless it’s a specific case of a
library that will be exclusively used by C++ callers; even then, you must consider
the issue of code duplication.
Now you know that if you need copy construction on your ref types, you must
implement it yourself. So, it may not be surprising to see in the next section that
the same holds true for copy-assignment operators.
1.5.4 Assignment operators
The copy-assignment operator is one that the compiler generates automatically
for native classes in standard C++, but this isn’t so for a ref class. The reasons are
similar to those that dictate that a copy constructor isn’t automatically generated.
The following code (using the Student class defined earlier) won’t compile:
Student s1("Nish");
Student s2;
s2 = s1; // error C2582: 'operator =' function
// is unavailable in 'Student'
Defining an assignment operator is similar to what you do in standard C++,
except that the types are managed:
Note that the copy-assignment operator can be used only by C++ callers, because
it’s invisible to other languages like C# and VB.NET. Also note that, for handle variables,
you don’t need to write a copy-assignment operator, because the handle
value is copied over intrinsically.
You should try to bring many of the good C++ programming practices you
followed into the CLI world, except where they aren’t applicable. As an example,
the assignment operator doesn’t handle self-assignment. Although it doesn’t
matter in our specific example, consider the case in >Listing 1.3.
In the preceding listing, assume that Grades is a class with a nontrivial constructor
and destructor; thus, in the Student class assignment operator, before the
m_grades member is copied, the existing Grades object is explicitly disposed by
calling delete on it—all very efficient. Let’s assume that a self-assignment occurs:
while(some_condition)
{
// studarr is an array of Student objects
studarr[i++] = studarr[j--]; // self-assignment occurs if i == j
if(some_other_condition)
break;
}
In the preceding code snippet, if ever i equals j, you end up with a corrupted
Student object with an invalid m_grades member. Just as you would do in standard
C++, you should check for self-assignment:
Student% operator=(const Student% s)
{
if(%s == this) <-- Check for self-assignment
{
return *this; <-- If it is so, return immediately
}
m_name = s.m_name;
if(m_grades)
delete m_grades;
m_grades = s.m_grades;
return *this;
}
We’ve covered some ground in this section—and if you feel that a lot of information
has been presented too quickly, don’t worry. Most of the things we’ve discussed
so far will come up again throughout this book; eventually, it will all make
complete sense to you. We’ll now look at boxing and unboxing, which are concepts
that I feel many .NET programmers don’t properly understand—with notso-
good consequences.
1.6 BOXING AND UNBOXING
Boxing is the conversion of a value of type V to an object of type V^ on the CLR
heap, which is a bit-wise copy of the original value object. Figure 1.4 native cod shows a diagrammatic
representation of the boxing process. Unboxing is the reverse process,
where an Object^ or a V^ is cast back to the original value type V. Boxing is an
implicit process (although it can be explicitly forced, as well), whereas unboxing is
always an explicit process. If it sounds confusing, visualize a real box into which
you put some object (say, a camera) so that you can send it via FedEx to your
friend the next city. The same thing happens in CLR boxing. When your friend
receives the package, they open the box and retrieve the camera, which is analogous
to CLR unboxing.
In this section, we’ll look at how boxing is an implicit operation in the new C++/
CLI syntax, how boxing ensures type safety, how boxing is implemented at the
MSIL level, and how to assign a nullptr to a boxed value type.
1.6.1 Implicit boxing in the new syntax
Whenever you pass a simple type like an int or a char to a method that expects an
Object, the int or char is boxed to the CLR heap, and this boxed copy is used by
the method. The reason is that ref types are always references to whole objects on
the CLR heap, whereas value types are typically on the stack or even on the native
C++ heap. When a method expects an Object reference, the value type has to be
copied to the CLR heap, where it must behave like a regular ref-type object. In the
same way, when the underlying value type has to be retrieved, it must be unboxed
back to the original value-type object. The internal boxing and unboxing mechanisms
are implemented by the CLR and supported in MSIL, so all the compiler
needs to do is emit the corresponding MSIL instructions.
In the old syntax, boxing was an explicit process using the __box keyword. Several
programmers complained about the extra typing required. Because most
people felt that the double-underscored keywords were repulsive, the fact that
they had to use one of those keywords a gratuitous number of times in the course
of everyday programming made them all the more upset. You can’t blame them,
as the code examples in Table 1.5 show.
It would be an understatement to say that the second code example is a lot more
pleasing to the eye and involves much less typing. But implicit boxing has a dangerous
disadvantage: It hides the boxing costs involved from the programmer,
which can be a bad thing. Boxing is an expensive operation; a new object has to
be created on the CLR heap, and the value type must be bitwise copied into this
object. Similarly, whenever a lot of boxing is involved, chances are good that
quite a bit of unboxing is also being performed. Unboxing typically involves creating
the original value type on the managed stack and bitwise copying its data
from the boxed object. As a developer, if you ignore the costs of repeated boxing/unboxing operations, either knowingly or unknowingly, you may run into performance
issues, most often in applications where performance is a major concern.
1.6.2 Boxing and type-safety
When you box a value type, the boxed copy is a separate entity from the original
value type. Changes in one of them won’t be reflected in the other. Consider the
following code snippet, where you have an int, a boxed object containing the int,
and a second int that has been explicitly unboxed from the boxed object:
int i = 100;
Object^ boxed_i = i; //implicitly boxed to Object^
int j = *safe_cast<int^>(boxed_i); //explicitly unboxed
Console::WriteLine("i={0}, boxed_i={1}, j={2}", i, boxed_i, j);
i++; j--;
Console::WriteLine("i={0}, boxed_i={1}, j={2}", i, boxed_i, j);
The first call to Console::WriteLine outputs
i=100, boxed_i=100, j=100
The second call outputs
i=101, boxed_i=100, j=99
As the output clearly indicates, they’re three different entities: the original value
type, the boxed type, and the unboxed value type. Notice how you had to
safe_cast the Object^ to an int^ before dereferencing it. This is because dereferencing
is always done on the boxed value type. To get an int, you have to apply
the dereference operator on an int^, and hence the cast.
NOTE: The safe_cast operator is new to C++/CLI and replaces __try_cast in
the old syntax. safe_cast is guaranteed to produce verifiable MSIL. You
can use safe_cast wherever you would typically use dynamic_cast,
reinterpret_cast, or static_cast. At runtime, safe_cast checks to
see if the cast is valid; if so, it does the conversion or else throws a
System::InvalidCastException.
When you box a value type, the boxed value remembers the original value type—
which means that if you attempt to unbox to a different type, you’ll get an
InvalidCastException. This ensures type-safety when you perform boxing and
unboxing operations. Consider Listing 1.4, which demonstrates what happens
when you attempt to unbox objects to the wrong value types.
This listing attempts to unbox a boxed double to an int and a boxed int to a
double. Although the code compiles, during runtime, an InvalidCastException
is thrown:
Unable to cast object of type 'System.Double' to type 'System.Int32'.
Unable to cast object of type 'System.Int32' to type 'System.Double'.
Note that type matching is always exact; for instance, you can’t unbox an int into
an __int64 (even though it would be a safe conversion).
1.6.3 Implementation at the MSIL level
MSIL uses the box instruction to perform boxing. The following is a quote from
the MSIL documentation: "The box instruction converts the raw valueType (an
unboxed value type) into an instance of type Object (of type O). This is accomplished
by creating a new object and copying the data from valueType into the
newly allocated object."
To get a better idea of how boxing is done, let’s look at how the MSIL is generated
(see Table 1.6).
We won’t decipher each MSIL instruction, but the line of code that is of interest is
the instruction at location IL_0004: box int32. The instruction before it, ldloc.0,
loads the contents of the local variable at the 0th position (which happens to
be the int variable i) into the stack. The box instruction creates a new Object,
copies the value (from the stack) into this object (using bitwise copy semantics),
and pushes a handle to this Object on the stack. The stloc.1 instruction pops
this Object from the stack into the local variable at the first position (the Object^
variable o). Table 1.7 shows how unboxing is done at the MSIL level.
Unboxing is the reverse process. The Object to be unboxed is pushed on the
stack, and a cast to int^ is performed using the castclass instruction. On successful
completion of this call, the Object on the stack is of type int^. The unbox
int32 instruction is executed, and it converts the boxed object (on the stack) to a
managed pointer to the underlying value type. This behavior is different from
boxing where a new object is created; however, unbox doesn’t create a new value
type instance. Instead, it returns the address of the underlying value type on the
CLR heap. The ldind.i4 instruction indirectly loads the value from the address
returned on the stack by the unbox instruction. This is basically a form of dereferencing.
Finally, the stloc.0 instruction stores this value in local variable 0, which
happens to be the int variable x.
The basic purpose of showing you the generated IL is to give you a better idea
of the costs involved in boxing/unboxing operations. When you box, you incur
the cost of creating a new Object. When copying the value type into this Object,
you waste CPU cycles as well as extra memory. When you unbox, you typically
have to safe_cast to your value type’s corresponding handle type, and the runtime
has to check to see if it’s a valid cast operation. Once you do that, the actual
unboxing reveals the address of the value type object within the CLI object, which
has to be dereferenced and the original value copied back.
Thus, both boxing and unboxing are very expensive operations. You probably
won’t see much of a performance decrease for simple applications, but bigger
and more complex applications may be seriously affected by performance loss if
you don’t restrict the number of boxing/unboxing operations that are performed.
Because boxing is implicit now, as a programmer you have to be that little
bit extra-cautious when you convert value types to ref types, either directly or
indirectly, as when you call a method that expects a ref-type argument with a
value type.
1.6.4 Assigning null to a boxed value type
An interesting effect of implicit boxing is that you can’t initialize a boxed value
type to null by assigning a 0 to it. You have to use the nullptr constant to accomplish
that:
int^ x1 = nullptr;
if(!x1)
Console::WriteLine("x1 is null"); // <-- this line is executed
else
Console::WriteLine("x1 is not null");
int^ x2 = 0;
if(!x2)
Console::WriteLine("x2 is null");
else
Console::WriteLine("x2 is not null");
// ^-- this line is executed
In the second case, the 0 is treated as an int, which is boxed to an int^. You specifically
need to use nullptr if you want to assign a handle to null. Note that the
compiler issues warning C4965 (implicit box of integer 0; use nullptr or explicit cast)
whenever it can.
When you have two overloads for a function that differ only by a value type
argument, with one overload using the value type and the other using the boxed
value type, the overload using the value type is given preference:
will call the Show(int) overload instead of the Show(int^) overload. Keep in mind
that, when selecting the best overload, the compiler gives lowest priority to one that
requires boxing. If you had another overload that required a nonboxing cast,
that overload would receive preference over the one that requires boxing. Given
three overloads, one that takes an int, one that takes a double, and one that takes
an int^, the order of precedence would be
To force an overload, you can do a cast to the argument type for that overload:
Show(static_cast(75));
Now that we’ve covered boxing and unboxing, I suggest that you always consciously
keep track of the amount of boxing that is done in your code. Because it’s
an implicit operation, you may miss out on intensive boxing operations; but
where boxing occurs, there is bound to be unboxing, too. If you find yourself having
to do a lot of unboxing, review your code to see if it overuses boxing, and try
to redesign your class to reduce the amount required. Take special care inside
loops, which is where most programmers end up with boxing-related performance
issues.
1.7 SUMMARY
In this chapter, we’ve covered some of the fundamental syntactic concepts of the
C++/CLI language. As you may have inferred by now, the basic programming
concepts remain the same in C++/CLI as in standard C++. However, you need to
accommodate for the CLI and everything that comes with it, such as garbage collection,
handles to the CLR heap, tracking references, and the implicit derivation
from System::Object. Topics we’ve covered included how to declare and instantiate
CLI types; how to use handles, and how they differ from pointers; and how
boxing and unboxing are performed when converting from value types to reference
types.
The designers of C++/CLI have gone to great lengths to ensure as close a similarity
to the standard C++ language as is practically possible, but some changes
had to be made due to the nature of the CLI (which is a different environment
from native C++). As long as you’re aware of those differences and write code
accordingly, you’ll do well.
Although C++/CLI’s biggest strength is its ability to compile mixed-mode
code, you need to be familiar with the core CLI programming concepts before
you can begin writing mixed-mode applications. With that view, in the next couple
of chapters we’ll explore the CLI features that are supported by C++/CLI,
such as properties, delegates and events, CLI arrays, CLI pointers, stack semantics,
function overriding, generics, and managed templates.
Master SharePoint with 3 eLearning Seminars Learn how to build a better SharePoint infrastructure and enable powerful collaboration with MVPs Dan Holme and Michael Noel. Register today!
SharePointConnections Conference Fall 2008 Don’t miss the premier event for Microsoft IT Professionals in Las Vegas, November 10-13. Register and book your room by August 25 and receive a FREE room night (based on a three night minimum stay).
VMworld 2008 - Sign Up Today! Join your peers on September 15-18 at The Venetian Hotel in Las Vegas as VMware hosts VMworld 2008, the leading Virtualization event.
Microsoft® Tech•Ed EMEA 2008 IT Professionals Advance your thinking with new ideas and practical real-world solutions at Microsoft’s FIVE day technical infrastructure conference 3-7 Nov., 2008. Register before 26 September 2008 to save €300.
Order Your SQL Fundamentals CD Today! Learn how to use SQL Server, understand Office integration techniques and dive into the essentials of SQL Express and Visual Basic with this free SQL Fundamentals CD.
Are You Really Compliant with Software Regulations? View this web seminar that will help you with compliance best practices and check out a management solution to assure that you won’t be in jeopardy of an audit.