The series continues today with functions. These are obviously core to any programming language, but it’s not obvious how many ways they differ from functions in C#. From compile-time execution to automatic return value types, there are a lot of differences to cover today.

Table of Contents

Declaration and Definition

Functions in C++ can be split into two parts. The first is the function declaration, which states its signature without stating how it works. To do this, simply add a semicolon after the signature:

int Add(int a, int b);

Now let’s write the second part: the function’s definition. This also contains the function’s signature but includes the body too:

int Add(int a, int b)
{
    return a + b;
}

So why does C++ have both a declaration and a definition when C# only has the definition? It mostly has to do with how C++ is compiled. We’ll cover that more in depth in the next article, but for now it’s important to know that C++ is compiled from top to bottom in a source file. A function definition or declaration makes it available to be referenced by code further down in the file. Here’s an approximation of what the compiler does:

// Start compiling here and read down the file's lines...
 
// There is no function `Add` at this point
// It doesn't matter that there is an `Add` later on
// This is a compile error
int four = Add(1, 3);
 
// Declaration of the `Add` function
// `Add` can now be referenced
int Add(int a, int b);
 
// Refers to `Add`, which the compiler knows about
// It doesn't matter that there's no definition yet
// The compiler trusts that the definition will come later
// If it doesn't, there will be an error
int three = Add(2, 1);
 
// Definition of the `Add` function
// The programmer has fulfilled the promise to define it
// The compiler now knows what to do when `Add` is called
int Add(int a, int b)
{
    return a + b;
}

This is also OK:

// Definition of the `Add` function
// `Add` can now be referenced
// Also says what to do when `Add` is called
int Add(int a, int b)
{
    return a + b;
}
 
// Refers to `Add`, which the compiler knows about
int three = Add(2, 1);

So we can use a function declaration to reorder our code, even though it’s compiled from top to bottom. We can just state the signature at the top and leave the body until later on. In the next article, we’ll talk about header files and this will become much more important. For now, it’s good to know that C++ functions are commonly seen with and without a declaration.

One last quirk: it’s possible to declare more than one function in a statement just like int a, b; declares two variables. This is very rarely seen and should generally be avoided in favor of the single-declaration form.

// Two functions:
//   int Add(int a, int b)
//   int Sub(int a, int b)
int Add(int a, int b), Sub(int a, int b);

As with variables, both functions share the same return type.

Optional Argument Names and Void

There’s a strange aspect of argument names in C++: they’re optional! This declaration is totally fine:

int Add(int, int);

After all, we’re just telling the compiler the signature of the function and the argument names are irrelevant to that. Perhaps more strangely, we can omit the argument names from function definitions!

// A very poor implementation of addition...
int Add(int a, int)
{
    return a + 1;
}

This is sometimes useful when a certain function signature is required but the arguments aren’t actually used in the body of the function. Consider a function meant to be used as an event handler:

void OnPlayerSpawned(Vector3)
{
    NumSpawns++;
}

This function doesn’t care where the player spawned because all it’s doing is keeping track of a statistic. So we can omit the argument name for a couple reasons. First, it tells the reader that this argument isn’t important in the function so it isn’t even given a name that needs to be memorized. Second, it tells the compiler not to complain about an unused variable. After all, we can’t use a varaible without a name in the first place. Sometimes we see a middle ground in C++ code where the name is stated inside a comment to gain only the second benefit but not the first:

void OnPlayerSpawned(Vector3 /* position */)
{
    NumSpawns++;
}

If the function takes no arguments at all, it may optionally state this explicitly by putting void where the parameters would normally go:

uint64_t GetCurrentTime(void);

Whether to add void or not is purely a stylistic choice.

Automatic Return Types

C++ variables can be declared with an auto type, similar to var in C#. In C++, function return types can also be declared as auto:

auto Add(int a, int b)
{
    return a + b;
}

Just like with variables, the compiler figures out what the return type should be. In this case, it’s just int since that’s what we get when adding two int values together.

We can also specify the return type after the argument list if we put auto before the function name:

auto Add(int a, int b) -> int
{
    return a + b;
}

In this case, we’re explicitly stating the return type. It is not automatically determined by the compiler, even though we have to still add auto before the function name. This alternative syntax is sometimes useful when the return type is very complex. We’ll see some examples later in the series when we tackle function pointers and templates.

Default Arguments

As in C#, default arguments are allowed as long as there aren’t any non-defaulted arguments after the first one. It’s a little different in C++ though due to split between declaration and definition. If the function has both, the default arguments are specified in the declaration:

// Function declaration states the default argument values
void SpawnPlayer(Vector3 position, float speed=0.0f);
 
// Function definition omits them
void SpawnPlayer(Vector3 position, float speed)
{
    // ...
}

If there’s no declaration, then the default arguments are added to the definition:

void SpawnPlayer(Vector3 position, float speed=0.0f)
{
    // ...
}
Variadic Functions

As in C#, functions may take a variable number of arguments. This works really differently in C++ though. It’s not deprecated like register variables are, but it’s often considered a bad practice to even use the feature. Still, let’s see how they look:

// The `...` comes after all the normal arguments
// It means "0 or more arguments of any types go here"
void PrintLog(LogLevel level, ...)
{
    // ...
}

The function should then call va_start, va_arg, and va_end in order to get the arguments. This is quite type-unsafe and a very clunky interface, which is part of why the feature should generally not be used. There are several alternatives that are preferred instead, but many are more advanced features that will be discussed later on in the series. For now, let’s discuss a simple one: overloading.

Overloading

As in C#, functions may be overloaded in the sense that more than one function may have the same name. When the function is called, the compiler figures out which of these identically-named functions should actually be called.

// Get the player's score given their ID
int GetPlayerScore(int playerId);
 
// Get the local player's score
int GetPlayerScore();
 
// Get the score of the player at a given position
int GetPlayerScore(Vector3 position);

These functions vary by the type of argument and the number of arguments. Now we can write code like this:

score = GetPlayerScore(myPlayerId);
score = GetPlayerScore();
score = GetPlayerScore(myPosition);

In this case, the compiler will generate calls to the three functions we declared in the same order.

Ref, Out, and In Arguments

In C#, arguments can be declared with the ref, in, and out keywords. Each of these change the argument to be a pointer to the passed value. In C++, these keywords don’t exist. Instead, we use some conventions:

// Alternative to `ref`
// Use an lvalue reference, which is like a non-nullable pointer
void MovePlayer(Player& player, Vector3 offset)
{
    player.position += offset;
}
 
// Alternative to `in`
// Use a constant lvalue reference
// `const` means it can't be changed
void PrintPlayerName(const Player& player)
{
    DebugLog(player.name);
}
 
// Alternative to `out`
// Just use return values
ReallyBigMatrix ComputeMatrix()
{
    ReallyBigMatrix matrix;
    // ...math goes here...
    return matrix
}
 
// Another alternative to `out`
// Use lvalue reference arguments
void ComputeMatrix(ReallyBigMatrix& mat1, ReallyBigMatrix& mat2)
{
    mat1 = /* math for mat1 */;
    mat2 = /* math for mat2 */;
}
 
// Another alternative to `out`
// Pack the outputs into a return value
tuple<ReallyBigMatrix, ReallyBigMatrix> ComputeMatrix()
{
    return make_tuple(/* math for mat1 */, /* math for mat2 */);
}

In the case of ref, C++ functions typically use an lvalue reference. If the reference needs to be nullable, which isn’t allowed for a C# ref argument, a pointer (Player*) can be used instead.

For in, a const lvalue reference is typically used. We haven’t covered const yet, but suffice to say it doesn’t allow changes to the variable. Writing player.score = 0; would cause a compiler error. This is broadly similar to what would happen with in arguments in C#. Again, a pointer (const Player* player) can be used if the parameter needs to sometimes be null.

out arguments are usually written by just returning them. In case more than one return value is needed, there are a couple of main options. First, lvalue reference arguments can be taken. This has the unfortunate downside that they can be read from before being assigned to so they might be inadvertently used as input. It’s also unclear to callers whether they’re providing arguments for input, output, or both. Second is the much-preferred option: pack all the outputs into a struct and return it. We haven’t talked about structs or templates, which are similar to C# generics, but the Standard Library’s tuple type and make_tuple helper function are shown here as roughly the alternatives to C# tuples.

Static Variables

Local variables within functions may be declared static. Similar to static fields of classes and structs in C#, this means that the variable has only one instance. A static C++ local variable has only one instance across all calls to the function:

int GetNextId()
{
    static int id = 0;
    id++;
    return id;
}
 
GetNextId(); // 1
GetNextId(); // 2
GetNextId(); // 3

In this example we have an id local variable that is static. There will be only one id across all calls to GetNextId. It’s like id is a global variable, but it can only be referred to within the function where it’s declared. This can be very convienient, but also be very surprising, just like static fields in C#.

Constexpr

Finally for today, functions may be marked with constexpr. This means that the function can be evaluated at compile time. For example:

constexpr int GetSquareOfSumUpTo(int n)
{
    int sum = 0;
    for (int i = 0; i < n; ++i)
    {
        sum += i;
    }
    return sum * sum;
}

This function can then be evaluated at compile time in order to generate a constant:

DebugLog(GetSquareOfSumUpTo(5000));
 
// equivalent to...
 
DebugLog(1020530960);

The function can also be evaluated at runtime, such as when its parameters are dependent on runtime values:

int n = file.ReadInt();
DebugLog(GetSquareOfSumUpTo(n));

This means that normal C++ can be reused for both compile time and runtime work. There’s usually no need to run scripts in another language in order to generate C++ files. The types and functionality the program is already made up of are usable at compile time with this mechanism.

There are some restrictions to what’s possible in a constexpr function though. Since they were introduced in C++11, each version has relaxed these restrictions. Still, some features like static local variables and goto aren’t allowed even in C++20.

Conclusion

Broadly speaking, functions are very similar between C# and C++. There are many differences though. These differences span syntactic quirks like how ref arguments are declared all the way to radically different features like compile-time function execution and static local variables. As we progress through the series, we’ll learn about many more types of functions including member functions (“methods”) and lambdas!

Next week we’ll go over how C++ is compiled. It’s quite different from C#!