Managed C++ is a lot more explicit than other .Net languages, this is good news because it makes your code clearer. Take pointers for example, all access to a .Net object is through a pointer. The pointer could be an object reference, or it could be an interface reference, but object access has to be through a pointer. There are five types of pointers you can use, four of them are indicated with the following keywords:
__box __gc __nogc __pin
The __pin
keyword is required for pinning pointers, the rest are
not usually required. A __nogc
pointer is used to point to unmanaged
data, the pointer will not be tracked by the GC. In general __nogc
pointers are incompatible with __gc
and __box
pointers.
A __gc
pointer is a pointer to a managed type, or a type on the
managed heap. A __gc
pointer is tracked by the runtime, so if you
copy a __gc
pointer the copy will be an object reference.
Value types (those marked with __value
, and what C# calls structs)
can live on the managed heap as long as they are part of a __gc
type, however value types are not created on the managed heap. If you use new
to create an instance of a value type then the unmanaged new will be used and
the instance will be created on the C++ heap, so for this type:
__value struct POINT { int x; int y; POINT(int i, int j):x(i), y(j){} String* ToString() { return String::Format(S"({0}, {1})", __box(x), __box(y)); } };
You can create an instance like this:
POINT __nogc* p = __nogc new POINT(1, 1);
Here, the pointer will always be an unmanaged pointer, so __nogc
is not required, but it is required to say that the type will be created with
the unmanaged new operator, hence the operator is called __nogc
new.
If I want to pass a POINT
to a method that takes a managed pointer
(for example to add it to a managed collection) then I will have to box it first.
In C++ you do this with the __box
operator which will return a
managed Object __gc*
pointer.
Later, when I extract the item from the collection I will get a System::Object
__gc*
pointer, so I will not be able to access the POINT
members. Instead, I need a typed managed pointer, a boxed pointer:
__box POINT* b = static_cast<__box POINT*>(arr->Item[0]); b->x += 5; Console::WriteLine(S"translated 5 units: {0}", b->ToString());
The pointer must be __box POINT*
because otherwise the compiler
will treat it as a POINT
__nogc*
pointer, and you
cannot convert between __gc
and __nogc
pointers.
You are restricted with what you can do with a __gc
pointer, you
cannot treat it like a normal C++ pointer, so it cannot be converted to an integer,
nor can you perform pointer arithmetic. If you need to pass get an unmanaged
pointer to a managed type, you first need to pin it by creating a pinning pointer.
Pinning means that the GC will not be able to move or collect the item pointed
to until the pinning pointer is destroyed, which means the scope of the current
method:
String __gc* str = S"Test"; String __pin* p = str; Console::WriteLine(S"Address {0:x8}", __box(reinterpret_cast<int>(p)));
Here, I want to print out the address of the string, so I first pin it by declaring
a pinning pointer. Once the object is pinned, I can convert to an unmanaged
type, which I do with the cast. However, there is not a version of WriteLine()
that takes a string and an int, so I have to box the int so that the version
with a string and an object is called. I can cast the pinned pointer to an unmanaged
pointer if I wish, which gives a mechanism for you to step through managed memory
to see how types are formatted on the managed heap. Note that the pinned pointer
is not managed, so if you make a copy the GC will not track the copy.
The final type of pointer I want to explain is an interior pointer. This is a managed pointer type, but as the name suggests it is not a pointer to a whole object. For example, consider this managed array:
Char c __gc [] = {'R', 'i', 'c', 'h', 'a', 'r', 'd'};
I can get an interior pointer to this by obtaining the address of the first item:
Char __gc* p = &c[0]; for (int i = 0; i < c->Length; i++, p++) Console::Write(__box(*p));
The pointer is a managed pointer, so if the GC moves the string in managed memory, it will change the interior pointer. You are allowed to perform integer arithmetic, as you can see from the loop, and not only are you able to dereference the interior pointer to read values, as you can see here, but you can also write to the managed memory too. I do not recommend that you write to memory on the managed heap because you could overwrite another object.
I hope that from this discussion you can see how pointers in Managed C++ give you more power than the other .Net languages, but they can also allow you to do a lot more damage. As an indication of this, try the sample code and see how easy it is to generate a fatal execution error!
// sample code #using <mscorlib.dll> using namespace System; // don't do this! __gc class BadInteriorPointers { __int64 x; __int64 y; String* s; public: BadInteriorPointers() { x=1; y=2; s=S"Test"; } void KillMe() { Dump(); // dump initial values // Get an interior pointer, use __int64* so that we // don't have to increment it by much to get to the string // member. __int64 __gc * p = &x; // change x through the interior pointer *p = 3; Dump(); // change y through the interior pointer p++; *p = 4; Dump(); // now do something nasty... p++; *p = 5; Dump(); } void Dump() { Console::WriteLine(S"{0} {1} {2}", __box(x), __box(y), s); } }; void main() { BadInteriorPointers* p = new BadInteriorPointers; p->KillMe(); }
Richard Grimes speaks at conferences and writes extensively on .NET, COM, and COM+. He is the author of Developing Applications with Visual Studio.NET (Addison Wesley, 2002). If you have comments about this topic, Richard can be reached at [email protected]. For questions regarding your newsletter subscription, please contact [email protected].