The GitHub project is closing in on supporting all the “must have” features. Today’s article tackles “boxing” and “unboxing” so our C++ game code will be able to convert types like int into an object and then convert an object back into an int. Usually we want to avoid this because it creates garbage for the GC to later collect and ruins type safety, but sometimes an API like Debug.Log insists that we pass it an object. Read on to see how to use boxing and unboxing in C++!

Table of Contents

First let’s review what boxing and unboxing are in C#. Say we have an int and we want to pass it to a function like Debug.Log that takes an object. We can make an object out of anything in C# by boxing it. This puts the int into a “box” of type object. Think of a class like this:

// The box type
class IntBox
{
	// The boxed value
	private int val;
 
	// Create the box with a boxed value
	public IntBox(int val)
	{
		this.val = val;
	}
 
	// Unbox by getting the boxed value back out
	int Unbox()
	{
		return val;
	}
}

Since all classes derive from object, we can use IntBox to convert an int into an object and then call Unbox to convert an object into an int. Here’s how that would look:

// Create a value to box
int val = 123;
 
// Box the value
// Store it as just an 'object'
object boxed = new IntBox(val);
 
// Unbox the value
int unboxed = ((IntBox)boxed).Unbox();

It would be tedious to have to create an IntBox for every type of primitive, enum, and struct. We could create a generic version of it, but we’d still end up with a lot of typing to box and unbox.

So the language designers of C# decided to add some syntax sugar to make boxing and unboxing easier. We don’t need to create a “box” class, we don’t need to explicitly call new on the box type to perform boxing, and we don’t need to cast object to the right type of “box” class to perform unboxing. Here’s how it looks with the syntax sugar in C#:

// Create a value to box
int val = 123;
 
// Box the value
// Store it as just an 'object'
object boxed = val;
 
// Unbox the value
int unboxed = (int)boxed;

Boxing is now implicit and unboxing is now a cast. As it turns out, this can be identically supported in C++. For boxing, all we have to do is add a constructor that takes the type to box. Likewise, we can define a conversion operator just like the overloading we did in part 13. Here’s how they look:

class Object
{
	// Boxing: construct from an int
	Object(int32_t val);
 
	// Unboxing: conversion operator to an int
	explicit operator int32_t();
};

Using this is just like in C#:

// Create a value to box
int32_t val = 123;
 
// Box the value
// Store it as just an 'Object'
Object boxed = val;
 
// Unbox the value
int32_t unboxed = (int32_t)boxed;

Boxing and unboxing work the same way in C# and C++ for all primitive types like int, enum types, and struct types.

So how do we implement support for boxing and unboxing? It comes down to implementing those two functions in Object. For boxing, we pass the value to C# to perform boxing. If the value to box is a managed struct, we just pass its handle. Then C# boxes the value, stores it in ObjectStore or StructStore<T>, and returns the handle. Here’s how the C++ part looks:

// Box a primitive, enum, or "full struct"
Object::Object(int32_t val)
{
	Handle = BoxInt(val);
}
 
// Box a "managed struct"
Object::Object(RaycastHit val)
{
	Handle = BoxRaycastHit(val.Handle);
}

Then the C# part looks like this:

// Box a primitive, enum, or "full struct"
static int BoxInt(int val)
{
	return ObjectStore.Store((object)val);
}
 
// Box a "managed struct"
static int BoxRaycastHit(int handle)
{
	return ObjectStore.Store((object)StructStore<RaycastHit>.Get(handle));
}

No JSON config file changes are necessary. The code generator outputs boxing and unboxing functions for all enum and struct types in the Types section. It also automatically generates boxing and unboxing functions for all primitive types like int, bool, and float.

With that, we have boxing and unboxing available to us. So now we can write C++ game code that takes advantage of it:

// Implicitly boxes the int32_t to an Object, just like in C#
Debug::Log(123);
 
void Foo(Object obj)
{
	// Unbox the Object into a QueryTriggerInteraction enum
	QueryTriggerInteraction unboxed = (QueryTriggerInteraction)obj;
}

It’s important to keep in mind that boxing and unboxing have their downsides. First and foremost is that boxing creates garbage for the GC to later collect. Think of the IntBox class we manually created and remember that is essentially what’s happening behind the syntax sugar in either C# or C++. It’s always good to keep garbage creation to a minimum to avoid the GC’s frame-spiking wrath, so beware of boxing in either language.

A boxed object has an unknown type. It’s just a plain object, which can be anything. Type safety goes mostly out the window by using plain object types as we’ll have to cast them to do anything with the actual type. This can lead to bugs and will definitely lead to slower code as runtime type checking needs to be applied.

In short, it’s good practice to avoid boxing and unboxing whenever possible. It is, however, useful to comply with APIs like Debug.Log which are highly useful and sometimes demand that types get boxed.

As usual, support for boxing and unboxing is now available on the GitHub project. Grab a copy if you want to try it out or see the nitty-gritty implementation details.