Features


Using the C++ Standard Library with Managed Types

Jonathan Caves

Yes, you can use STL with Managed C++ garbage-collected types. Really.


Although the Base Class library that ships with the .NET platform does come with its own containers and manipulators, they are in general not as powerful or as optimal as their counterparts in the C++ Standard library (previously known as STL). So, is it possible to use a C++ container like std::vector with a managed type like System::String? Can templates and managed types work well together? This article explores the issues and shows that with a little bit of care and attention it is indeed possible to mix types from the two worlds.

What We’d Like to Be Able to Do

“So,” you may ask, “what’s stopping me from just writing code like the following?”

#using <mscorlib.dll>
#include <vector>

using namespace System;
using namespace std;

int main()
{
  vector<String *> fruit;

  fruit.push_back(S"Peach");
  fruit.push_back(S"Apple");
  fruit.push_back(S"Banana");
  fruit.push_back(S"Orange");
  fruit.push_back(S"Apricot");

  for (vector<String *>::iterator iter =
    fruit.begin();
    iter != fruit.end(); iter++) {
    Console::WriteLine(*iter);
  }
}

The problem is that in the managed world there is a garbage collector, and in order for the garbage collector to work correctly, it needs to know the location of every reference to every managed object. Unfortunately, the garbage collector knows nothing about the C++ heap. When vector allocates internal storage, it does so from the C++ heap, and when it then stores a reference to a managed String in this memory, the garbage collector loses track of String.

If in the simple program above you forced the garbage collector to wake up and do its stuff by adding a call to:

System::GC::Collect();

then the collector would notice that there are no references to any of the String objects you added to vector, so it would be free to collect the memory. If at a later point in the program you tried to access one of these String objects, then the program would crash — a classic attempt to access freed memory.

Containers of Managed Types: Solving the Problem

So what to do? Can you somehow tell the garbage collector that there are references in the C++ heap to managed objects?

Fortunately, the Base Class library does provide the necessary functionality. The class GCHandle exists exactly for this purpose. To make it easy to use, the C++ compiler team has created a class template, gcroot, which encapsulates the functionality. The definition of the class template can be found in the header file gcroot.h. See Listing 1 for a slightly simplified version, minus some #ifdefs.

The gcroot class template is used to wrap a managed type. gcroot inserts the appropriate calls to register and deregister references in the C++ heap to the managed object, seamlessly enabling a managed type to be used in a C++ container like vector.

So you just update the original code to use the gcroot class:

#using <mscorlib.dll>
#include <vector>
#include <gcroot.h>

using namespace System;
using namespace std;

typedef gcroot<String *> wrappedString;

int main()
{
  vector<wrappedString> fruit;

  fruit.push_back(S"Peach");
  fruit.push_back(S"Apple");
  fruit.push_back(S"Banana");
  fruit.push_back(S"Orange");
  fruit.push_back(S"Apricot");

  for (vector<wrappedString>::iterator
    iter =
    fruit.begin();
    iter != fruit.end(); iter++) {
    Console::WriteLine(*iter);
  }
}

You’ll notice that even though the element type of vector is now gcroot<String *>, you can continue to use the iterator as before. This is because the gcroot class template has a user-defined conversion operator:

operator T() const

So gcroot<String *> has an implicit conversion to String *, and you do not need to add an explicit cast to the code above.

Using Standard Algorithms on Containers of Managed Types

The same ease of use you get for the containers applies to the algorithms in the C++ Standard library. For example, replace the use of for with a call to for_each in the above example. First, you define a Print function:

void Print(const wrappedString &wstr)
{
  Console::WriteLine(wstr);
}

and then we use it as usual:

for_each(fruit.begin(), fruit.end(), Print);

Again the user-defined conversion takes care of the implicit conversion from a gcroot<String *> to a String *.

You can also sort the String objects — for this you need to define a less-than operator for your wrapped String:

bool operator<(const wrappedString &wstr1,
const wrappedString &wstr2)
{
  return String::Compare(wstr1, wstr2) < 0;
}

and then we can just sort our vector of Strings:

sort(fruit.begin(), fruit.end());

Putting It All Together

You can now assemble the pieces to get:

#using <mscorlib.dll>
#include <vector>
#include <algorithm>
#include <gcroot.h>

using namespace System;
using namespace std;

typedef gcroot<String *> wrappedString;

void Print(const wrappedString &wstr)
{
  Console::WriteLine(wstr);
}

bool operator<(const wrappedString &wstr1,
  const wrappedString &wstr2)
{
  return String::Compare(wstr1, wstr2) < 0;
}

int main()
{
  vector<wrappedString> fruit;

  fruit.push_back(S"Peach");
  fruit.push_back(S"Apple");
  fruit.push_back(S"Banana");
  fruit.push_back(S"Orange");
  fruit.push_back(S"Apricot");

  Console::WriteLine(S"Unsorted:");

  for_each(fruit.Begin(), fruit.End(), Print);

  sort(fruit.begin(), sort.end());

  Console::WriteLine();
  Console::WriteLine(S"Sorted");

  for_each(fruit.Begin(), fruit.End(), Print);
}

The output is:

Unsorted:
Peach
Apple
Banana
Orange
Apricot

Sorted:
Apple
Apricot
Banana
Orange
Peach

Summary

With the use of the class template gcroot, it is possible to use the full functionality of the Standard C++ library with managed types like System::String.

Jonathan Caves is Microsoft’s principal representative at the ANSI C++ standards committee. He has been a member of Microsoft’s Visual C++ group since 1993 and is currently a lead software engineer for Visual C++.