We’ve been able to call methods since the very beginning, but we’ve always had to pass all the parameters. Today we’ll add support for default parameters so you can skip them sometimes. There’s a surprising amount of detail involved with this, so read on to learn some caveats of C#, .NET, and C++.

Table of Contents

Let’s first have a refresher about the rules for default parameters in C# so we know what to support in C++. The basic concept is that you add = X to the parameters of a function:

string Stringify(int val, int base = 10)
{
	// ...
}

Here we have base as a “default parameter.” That means that it’s optional to pass a value when calling Stringify:

string hexadecimal = Stringify(123, 16);
string decimal = Stringify(123, 10);
string alsoDecimal = Stringify(123);

In the last call, the default value of 10 is used because we didn’t pass a parameter. The compiler simply rewrites the last call to look exactly the same as the second call. There are no hidden conditionals (e.g. an if) and the compiler doesn’t generate two versions of the function: one with one parameter and another with two parameters.

The next rule is that default parameters must come after non-default parameters. So we couldn’t reverse the order of the parameters like this:

string Stringify(int base = 10, int val) // compiler error
{
	// ...
}

Note that this doesn’t mean that the default parameters have to be the last parameters of the function. There is one special case where we can add another parameter after the default parameters: params a.k.a. “var args”.

string Stringify(int base = 10, params int[] vals)
{
	// ...
}

A params parameter is always an array and always the last parameter, even after default parameters. It can also be passed either explicitly or implicitly. For explicit parameter passing, we just pass an array. For implicit parameter passing, an array will be created for us by the compiler even if we don’t pass any parameters. Let’s look at the possible scenarios:

// Explicit parameter passing for both
Stringify(10, new int[] { 1, 2, 3 });
 
// Explicit parameter passing for both
// base = 10
// vals = new int[] { 1, 2, 3 }
Stringify(10, 1, 2, 3);
 
// Explicit parameter passing for default parameter
// Implicit parameter passing for 'params'
Stringify(10);
 
// Implicit parameter passing for both
Stringify();

Next up there are rules about what we can specify for the default parameter values. It’s the same rules for const, so it’s pretty easy to remember. The value has to be a constant integer (8-, 16-, 32-, or 64-bit), boolean, floating-point (32- or 64-bit), character, enumerator of an enum, string, or null. So we can’t use any variables that aren’t known at compile time, including static or instance variables of a class.

The rules in C++ are very similar and most of the time the code generator can simply output the same = X syntax as in the C# code.

String Stringify(int32_t val, int32_t base = 10)
{
	// ...
}

Using a function with a default parameter in C++ is also the same:

String hexadecimal = Stringify(123, 16);
String decimal = Stringify(123, 10);
String alsoDecimal = Stringify(123);

We can do this for integers, booleans, floating-point values, characters, and enum enumerators without any issues at all. Strings and null cause us some issues though.

Remember from part 20 that we pass references in C++ for class and “managed struct” (i.e. structs with class fields) types:

int32_t GetNumLines(String& str)
{
	// ...
}

Reference parameters require a variable to “take the reference of,” so we can’t just use literal values because there’s no such variable. This simply won’t work:

int32_t GetNumLines(String& str = nullptr) // compiler error
{
	// ...
}

Instead, we can introduce a global variable that is a “null” string. We put this in Bindings.h:

namespace Plugin
{
	extern String NullString;
}

That extern tells the compiler to not actually generate a variable but just proceed as though there were such a variable. We’re basically making a promise to the linker that we’ll define that variable in a .cpp file, so let’s put it in Bindings.cpp:

namespace Plugin
{
	String NullString(nullptr);
}

Now we’ve actually defined the global variable, so we won’t get any linker errors. The reason we have to do this lies in how C++ is compiled. If we simply define the global variable in Bindings.h and multiple .cpp files #include it, as Bindings.cpp and Game.cpp do, then the variable will be defined in both .cpp files and there will be a conflict at link-time.

Now that we have the NullString variable available, we can use it as a default parameter:

int32_t GetNumLines(String& str = Plugin::NullString)
{
	// ...
}

C++ is more lenient with its rules for default parameters in this case. We’re using a variable, not a constant, as the default parameter and that’s allowed.

As it turns out, there are no APIs in either .NET or Unity that have non-null default values of strings. Technically, you could have a string str = "hello" parameter, but this never happens. The same goes for having null default values of non-strings, such as a Queue queue = null parameter. This also never happens in the .NET or Unity APIs. So we’ll skip implementing support for these at the moment. Since we don’t have support for params parameters either, we’ll also skip support for default parameters when these are present.

As usual, this support for default parameters is now available in the GitHub project. Feel free to check it out and leave comments if you’ve got any questions or comments.