C++ For C# Developers: Part 4 – Functions
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
- Part 1: Introduction
- Part 2: Primitive Types and Literals
- Part 3: Variables and Initialization
- Part 4: Functions
- Part 5: Build Model
- Part 6: Control Flow
- Part 7: Pointers, Arrays, and Strings
- Part 8: References
- Part 9: Enumerations
- Part 10: Struct Basics
- Part 11: Struct Functions
- Part 12: Constructors and Destructors
- Part 13: Initialization
- Part 14: Inheritance
- Part 15: Struct and Class Permissions
- Part 16: Struct and Class Wrapup
- Part 17: Namespaces
- Part 18: Exceptions
- Part 19: Dynamic Allocation
- Part 20: Implicit Type Conversion
- Part 21: Casting and RTTI
- Part 22: Lambdas
- Part 23: Compile-Time Programming
- Part 24: Preprocessor
- Part 25: Intro to Templates
- Part 26: Template Parameters
- Part 27: Template Deduction and Specialization
- Part 28: Variadic Templates
- Part 29: Template Constraints
- Part 30: Type Aliases
- Part 31: Deconstructing and Attributes
- Part 32: Thread-Local Storage and Volatile
- Part 33: Alignment, Assembly, and Language Linkage
- Part 34: Fold Expressions and Elaborated Type Specifiers
- Part 35: Modules, The New Build Model
- Part 36: Coroutines
- Part 37: Missing Language Features
- Part 38: C Standard Library
- Part 39: Language Support Library
- Part 40: Utilities Library
- Part 41: System Integration Library
- Part 42: Numbers Library
- Part 43: Threading Library
- Part 44: Strings Library
- Part 45: Array Containers Library
- Part 46: Other Containers Library
- Part 47: Containers Library Wrapup
- Part 48: Algorithms Library
- Part 49: Ranges and Parallel Algorithms
- Part 50: I/O Library
- Part 51: Missing Library Features
- Part 52: Idioms and Best Practices
- Part 53: Conclusion
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#!
#1 by Maxime on June 8th, 2020 ·
Hi !
Thanks for this good article !
PS : Don’t forget to update the table of contents of the others parts :p
#2 by SH42913 on June 12th, 2020 ·
Great thanks for the article! Waiting for next!
#3 by Ali on January 30th, 2021 ·
It is a great piece of article. So much information, easy to read and follow for a c# programmer.
Thanks
#4 by crispcat on June 24th, 2021 ·
Thanks for article. Really enjoy it.
Found a little typo.
“Writing player.score = 0; would cause a compiler error. This is broadly similar to what would happen with out arguments in C#.”
I think you mean in argument. Out argument should be set before return.
#5 by jackson on July 3rd, 2021 ·
Thanks for letting me know about this! I’ve updated the article to fix the typo.
#6 by Van on September 11th, 2021 ·
Thank you, this is a nice resource. I do have a small suggestion though. Would you please add a link to the next section at the bottom of the article? Currently the reader must scroll back up to the top and search for it in the table of contents.
#7 by Peter on August 24th, 2022 ·
Thank you for your hard work! This is a awesome resource.
#8 by Valentin on January 21st, 2023 ·
Hey ! Thanks for this amazing resource !
I have a question regarding the optional parameter declaration.
Why stating a parameter type if we are not gonna use it (void OnPlayerSpawned(Vector3))
Can we not just put no params ?
#9 by jackson on January 22nd, 2023 ·
If you put no params then your function has a different signature– one with no parameters– and isn’t compatible with functions that have parameters. The main reason for not naming parameters is so you can keep them while maintaining compatibility but letting readers and the compiler know that you won’t be using them. There are at least two common scenarios where this happens. First, callbacks:
Second, abstract base classes (like interfaces in C#):
Note that both of these scenarios are the same in C# except that the callbacks would be a delegate type like
Func<Response>
instead of a function pointer.#10 by Valentin on January 24th, 2023 ·
I see. Thanks for your response :)
That’s really helpfull