Using % and &

This post brings together three related questions and answers posted recently on comp.lang.c++.moderated. They all have to do with the relationship between % (tracking reference) and & (unmanaged reference).

First, Dietmar Kuehl asked about this C++/CLI code:

template <typename T> void f(T&) {}

int main() {
  int^ ptr = gcnew int(0);
  f(*ptr);  // error
}

The solution is to change f's parameter from T& to T% . When referring to ab object on the GC heap, just as instead of an unmanaged pointer you need a handle ^ (which tracks during GC) you also instead of an unmanaged reference need a tracking reference % (which tracks during GC).

Trying to pass a deref'd ^ (which tracks) to a & (which doesn't track) is the same kind of error as trying to pass a const object (which is const) to a non-const & (which isn't const):

template <typename T> void f(T&) {}

int main() {
  const int i;
  f(i);   // similar category of error
}

So in general, to be able to bind also to objects on the GC heap, a function taking a & parameter only needs to change the & to % instead (the body generally won't care).

In our STL.NET project we're providing the standard algorithms with % instead of & parameter types, which doesn't affect the meaning but allows them to be used with all the types they work with already plus also managed types.

If you can't change the function and must call a function with a plain & (including if you just want to call the standard algorithms directly for some reason), you can still do it by using the RAII pin_ptr so that tracking isn't needed for the duration:

template <typename T> void f(T&) {}

int main() {
  int^ ptr = gcnew int(0);
  f( *pin_ptr<int>(&*ptr) );
}

Note that normally you don't need that extra &* -- here it's needed because we're dealing with a boxed value type and want to reach into the box.

Separately, Sebastian Kaliszewski asked about this code:

template <typename T> void f(T%) {}

int main() {
  int* ptr = new int(0);
  f(^ptr);
}

This code does work, except that you dereference a ^ with * (just like you dereference all pointerlike abstractions), so change the call to f(*ptr); and it works just fine.

Finally, Thorsten Ottosen asked whether the following couldn't be possible:

template< class T > void foo( T& );
template< class T > void bar( T% );

int main()
{
   int^ gc  = gcnew int(0);
   int* ptr = new int(0);
   foo( *gc ); // 1
   foo( *ptr ); // 2
   bar( *gc ); // 3
   bar( *ptr ); // 4
}

The answer is that all of those work as written, except for #1 which only needs a pin_ptr. A pin_ptr is an RAII helper that pins an object on the GC heap for the lifetime of the pin_ptr. Pinning ensures that the GC doesn't move the object around, during which time it's safe to take a plain old unmanaged pointer to it:

   foo( *pin_ptr<int>(&*gc) ); // 1 (fixed)

Or, a little more clearly:

   pin_ptr<int> pp = &*gc;  // take a pin
   f( *pp );                // call any old function taking * or &

The % can bind to any object whether the object moves (i.e., is on the gc heap) or not -- thus % can bind to a strict superset of what a & can bind to. (The & can bind directly to any object not on the gc heap, but requires a pin to bind to one on the gc heap.)

Thorsten also asked:

What has been the rationale for the current design?

In short, to provide pointerlike and referencelike abstractions that work for the general case of garbage-collected memory, which includes compacting GCs that move objects around (so the new pointer/reference abstractions have to be able to track), as close to unmanaged pointers/references in function and syntax as possible.