Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tracking reference in C++/CLI

Tags:

c++-cli

Can someone please explain me the following code snippet?

value struct ValueStruct {
    int x;
};

void SetValueOne(ValueStruct% ref) {
    ref.x = 1;
}

void SetValueTwo(ValueStruct ref) {
    ref.x = 2;
}

void SetValueThree(ValueStruct^ ref) {
    ref->x = 3;
}

ValueStruct^ first = gcnew ValueStruct;
first->x = 0;
SetValueOne(*first);

ValueStruct second;
second.x = 0;
SetValueTwo(second); // am I creating a copy or what? is this copy Disposable even though value types don't have destructors?

ValueStruct^ third = gcnew ValueStruct;
third->x = 0;
SetValueThree(third); // same as the first ?

And my second question is: is there any reason to have something like that?:

ref struct RefStruct {
    int x;
};

RefStruct% ref = *gcnew RefStruct;
// rather than:
// RefStruct^ ref = gcnew RefStruct;

// can I retrieve my handle from ref?
// RefStruct^ myref = ???

What is more: I see no difference between value type and ref type, since both can be pointed by handler ;(

like image 573
Marc Andreson Avatar asked Aug 01 '10 00:08

Marc Andreson


1 Answers

Remember that the primary use of C++/CLI is for developing class libraries for consumption by GUIs / web services built in other .NET languages. So C++/CLI has to support both reference and value types because other .NET languages do.

Furthermore, C# can have ref parameters that are value typed as well, this isn't unique to C++/CLI and it doesn't in any way make value types equivalent to reference types.

To answer the questions in your code comments:

am I creating a copy or what?

Yes, SetValueTwo takes its parameter by value, so a copy is made.

is this copy Disposable even though value types don't have destructors?

Incorrect. Value types can have destructors. Value types cannot have finalizers. Since this particular value type has a trivial destructor, the C++/CLI compiler will not cause it to implement IDisposable. In any case, if a parameter is an IDisposable value type, the C++/CLI compiler will ensure that Dispose is called when the variable goes out of scope, just like stack semantics for local variables. This includes abnormal termination (thrown exception), and allows managed types to be used with RAII.

Both

ValueStruct% ref = *gcnew ValueStruct;

and

ValueStruct^ ref = gcnew ValueStruct;

are allowed, and put a boxed value type instance on the managed heap (which isn't a heap at all, but a FIFO queue, however Microsoft chooses to call it a heap like the native memory area for dynamic allocation).

Unlike C#, C++/CLI can keep typed handles to boxed objects.

If a tracking reference is to a value type instance on the stack or embedded in another object, then the value type content has to be boxed in the process of formed the reference.

Tracking references can also be used with reference types, and the syntax to obtain a handle is the same:

RefClass^ newinst = gcnew RefClass();
RefClass% reftoinst = *newinst;
RefClass^% reftohandle = newinst;

RefClass stacksem;
RefClass^ ssh = %stacksem;

One thing that I can never seem to remember completely is that the syntax isn't 100% consistent compared to native C++.

Declare a reference:

int& ri = i; // native
DateTime% dtr = dt; // managed tracking reference

Declare a pointer:

int* pi; // native
Stream^ sh; // tracking handle

Form a pointer:

int* pi = &ri; // address-of native object
DateTime^ dth = %dtr; // address-of managed object

Note that the unary address-of operator is the same as the reference notation in both standard C++ and C++/CLI. This seems to contradict a tracking reference cannot be used as a unary take-address operator (MSDN) which I'll get back to in a second.

First though, the inconsistency:

Form a reference from a pointer:

int& iref = *pi;
DateTime% dtref = *dth;

Note that the unary dereference operator is always *. It is the same as the pointer notation only in the native world, which is completely opposite of address-of which, as mentioned above, are always the same symbol as the reference notation.

Compilable example:

DateTime^ dth = gcnew DateTime();
DateTime% dtr = *dth;

DateTime dt = DateTime::Now;
DateTime^ dtbox = %dt;

FileInfo fi("temp.txt");
// FileInfo^ fih = &fi;  causes error C3072
FileInfo^ fih = %fi;

Now, about unary address-of:

First, the MSDN article is wrong when it says:

The following sample shows that a tracking reference cannot be used as a unary take-address operator.

The correct statement is:

% is the address-of operator for creation of a tracking handle. However its use is limited as follows:

A tracking handle must point to an object on the managed heap. Reference types always exist on the managed heap so there is no problem. However, value types and native types may be on the stack (for local variables) or embedded within another object (member variables of value type). Attempts to form a tracking handle will form a handle to a boxed copy of the variable: the handle is not linked to the original variable. As a consequence of the boxing process, which requires metadata which does not exist for native types, it is never possible to have a tracking handle to an instance of a native type.

Example code:

int i = 5;
// int^ ih = %i;  causes error C3071

System::Int32 si = 5;
// System::Int32^ sih = %si; causes error C3071
// error C3071: operator '%' can only be applied to an instance 
//              of a ref class or a value-type

If System::Int32 isn't a value type then I don't know what is. Let's try System::DateTime which is a non-primitive value type:

DateTime dt = DateTime::Now;
DateTime^ dtbox = %dt;

This works!

As a further unfortunate restriction, primitive types which have dual identity (e.g. native int and managed value type System::Int32) are not handled correctly, the % (form tracking reference) operator cannot perform boxing even when the .NET name for the type is given.

like image 79
Ben Voigt Avatar answered Nov 18 '22 15:11

Ben Voigt