Last week’s article covered delegates, so it’s only natural that we follow up this week by covering events. Supporting delegates has laid a good foundation for supporting events, so let’s dive in and see how to implement and use them in C++.

Table of Contents

First let’s recap delegates and events from a purely C# perspective: no C++. Delegates are a class type that represent zero or more functions. Unlike interfaces or classes, these can be any functions: static methods, instance methods, lambdas, or anonymous functions. They can use generic types for their parameters and return values. And we can even define them as “top level” types like classes, structs, interfaces, and enums. Here are some quick examples:

// Define a delegate that takes no parameters and returns void
delegate void Runnable();
 
// Use a Runnable
void Foo()
{
	Debug.Log("Foo called");
}
Runnable runnable = Foo;
 
// Define a delegate that takes two floats and returns a bool
delegate bool FloatOp(float a, float b);
 
// Use a FloatOp
bool IsGreater(float a, float b)
{
	return a > b;
}
FloatOp floatOp = IsGreater;
 
// Define a delegate that has generic parameters and return values
delegate TReturn BinaryFunc<TParam1, TParam2, TReturn>(TParam1 param1, TParam2 param2);
 
// Use a BinaryFunc
uint Add(byte a, ushort b)
{
	return (uint)(a + b);
}
BinaryFunc<byte, ushort, uint> binaryFunc = Add;

Delegate types internally keep track of the functions to call when they are called. In all of the examples above, there’s just one function to call. However, we can add and remove more functions like this:

// Define the delegate
delegate void Runnable();
 
// Define a couple of functions usable with the delegate
void Foo()
{
	Debug.Log("Foo called");
}
void Bar()
{
	Debug.Log("Bar called");
}
 
// Initialize the delegate with just one function
Runnable runnable = Foo;
 
// Calling the delegate calls just the one function
runnable(); // prints: "Foo called"
 
// Add a function. Calling the delegate now calls both.
runnable += Bar;
runnable(); // prints "Foo called" and "Bar called"
 
// Remove a function. Calling the delegate just calls the one we didn't remove.
runnable -= Foo;
runnable(); // prints "Bar called"

Keep in mind that this is all just delegates. Events haven’t entered the picture yet. It’s a common misconception that events are required for more than one function to be called when a delegate is called (a.k.a. invoked).

Now let’s layer on events. Events only do one thing: add syntax sugar for two special functions. Imagine we wanted users of our class, struct, or interface to be able to add functions to and remove functions from the delegate. We would have to define functions like this:

public class Button
{
	// Define a delegate for when clicks happen
	public delegate void ClickHandler();
 
	// Keep an instance of the delegate
	private ClickHandler onClick;
 
	// Allow users of the Button to add and remove functions to the delegate
	void AddClickHandler(ClickHandler handler)
	{
		onClick += handler;
	}
	void RemoveClickHandler(ClickHandler handler)
	{
		onClick -= handler;
	}
}
 
// User code
Button loginButton = new Button();
loginButton.AddClickListener(HandleLoginButtonClicked);
void HandleLoginButtonClicked()
{
	// ... do log in
}

AddClickHandler and RemoveClickHandler allow Button to maintain “encapsulation” by tightly controlling the access to the onClick delegate field. If Button were to make onClick into a public field or property, users of Button would have uncontrolled access to it. That would mean that they could write code like button.onClick = null and clear all the functions that have been added to the delegate.

The language designers of C# thought “add” and “remove” functions were such a good idea that they gave them their own keyword: event. By adding the event keyword to the delegate field, we automatically get AddClickHandler and RemoveClickHandler plus some special syntax for users of our class, struct, or interface. Here’s how it looks:

public class Button
{
	// Define a delegate for when clicks happen
	public delegate void ClickHandler();
 
	// Keep an instance of the delegate as an event
	public event ClickHandler onClick;
}
 
// User code
Button loginButton = new Button();
loginButton.onClick += HandleLoginButtonClicked;
void HandleLoginButtonClicked()
{
	// ... do log in
}

The differences here are that the field is now public and has the event keyword. AddClickHandler and RemoveClickHandler are essentially automatically generated by the compiler. To call them, users use special syntax sugar like this: button.onClick += del and button.onClick -= del. Other than that, nothing has changed. It’s just a more concise way of writing the same code.

So what if we wanted to be able to write AddClickHandler and RemoveClickHandler functions that did more than just add and remove functions to the delegate? Just like with auto-properties (e.g. public string Name { get; set; }), we can define our own add and remove functions for events very similarly to defining our own get and set for properties.

public class Button
{
	// Define a delegate for when clicks happen
	public delegate void ClickHandler();
 
	// Keep an instance of the delegate (not as an event)
	private ClickHandler onClick;
 
	// Define an event for the delegate
	public event ClickHandler OnClick
	{
		add
		{
			Debug.Log("Added to OnClick");
			onClick += value;
		}
		remove
		{
			Debug.Log("Removed from OnClick");
			onClick -= value;
		}
	}
}

This is just like properties where we have a private “backing field” and a public property with custom get and set functions. We even get an implicit parameter named value. The differences are that they’re named add and remove, use the event keyword, only work on delegate types, and users call them with += and -= instead of obj.Name and obj.Name = "Jackson".

With this understanding of events, it’s easy to see that exposing them to C++ is going to be easy. First we’ll need a new section in the code generator’s JSON config to define which events to allow C++ to use. This goes in the Types block alongside the sections for fields, properties, methods, and constructors:

{
	"Name": "UnityEngine.Application",
	"Events": [
		{
			"Name": "onBeforeRender"
		}
	]
},

The above JSON snippet tells the code generator that we want C++ code to have access to the onBeforeRender event in Unity’s Application class. Of course we’ll also need to specify JSON for the UnityAction delegate type to make the event usable:

"Delegates": [
	{
		"Type": "UnityEngine.Events.UnityAction"
	}
]

Now we can write C++ game code to use the event:

// Define a delegate type
struct BeforeRenderHandler : UnityAction
{
	// Define the function to call when the delegate is invoked
	void operator()() override
	{
		Debug::Log(String("Before render handler called"));
	}
};
 
// Create a delegate
BeforeRenderHandler handler;
 
// Add the delegate to the event. Equivlanet to +=.
Application.AddOnBeforeRender(handler);

The explicit AddX and RemoveX method names are used in C++, just like the explicit GetX and SetX names are used with properties. Properties are really similar to events, so this symmetry makes sense.

When using delegates and events in C++, it’s important to keep in mind the lifecycle of the event object. Unlike C# where the memory manager is keeping our object alive until it’s garbage-collected, C++ has no memory manager or garbage collector to do this for us. That’s for better and for worse. On the “better” side, it’s easier to write casual C# code that doesn’t really care where the memory is, when it gets freed, and how much time garbage collection takes. On the “worse” side, it’s impossible to write C# code that cares about any of these things. It’s a little reminder of the sort of doors that scripting in C++ opens for us.

That said, let’s look briefly at how C++ object lifecycles work. This will help us avoid situations where we think that our delegate should be called but actually isn’t because the delegate object is dead. The main thing to keep in mind is that a C++ object is destructed when it goes out of scope. So this code will not result in the delegate getting called:

void Foo()
{
	// Create a delegate
	// Its constructor is called
	// This creates the C# delegate
	BeforeRenderHandler handler;
 
	// Add the delegate to the event
	// The event's delegate gets a reference to the C# delegate for 'handler'
	Application.AddOnBeforeRender(handler);
 
	// The function ends and 'handler' goes out of scope
	// This calls the destructor for 'handler'
	// This sets a flag in C# to not call the C++ delegate
	// This is good because it's dead now
	// This is bad because we won't get called back
}

Note that there isn’t any code for the last set of comments. Merely having handler go out of scope at the next } is enough to trigger its destructor. It’s essential to keep this in mind: class objects go away just like other local variables when their scope ends.

To work around this, we need a way to store variables long-term. Instead of storing them on the “stack” like other temporary local variables, we want to decouple event handlers like this from the lifecycle of the function that’s creating them. We need to use the heap for this, and that’s easily done in C++. The new operator allocates space on the heap and calls the constructor. We can use it like this:

void Foo()
{
	// Create a delegate on the heap. We get a pointer (*) to it.
	// Its constructor is called
	// This creates the C# delegate
	BeforeRenderHandler* handler = new BeforeRenderHandler();
 
	// Add the delegate to the event by dereferencing (*handler)
	// The event's delegate gets a reference to the C# delegate for 'handler'
	Application.AddOnBeforeRender(*handler);
 
	// The function ends and 'handler' goes out of scope
	// But it's just a pointer, so its destructor isn't called
}

Now that the object continues to live on the heap beyond the end of the function, it’s important to make sure we later on call delete handler to free up its memory or we’ll have a leak on our hands. This means we’ll need to keep track of handler and delete it at the right time. We can also explicitly say where on the heap handler should be allocated using “placement new”: new (address) BeforeRenderHandler(). So with C++ we have total control over where the memory is allocated, when it is freed, and how it is freed.

With that, we now have support for events in C++. It’s a simple addition onto delegates once we’ve understood what events in C# really are: syntax sugar around two functions. However, they’re used pretty widely in C# APIs, so supporting them is important to make C++ a viable scripting alternative to C#. Check out the GitHub project to get access to the support for events or to dig into the nitty-gritty details of how they’re implemented.