We've seen how the execution of our functions can be controlled by conditional statements. These conditional statements allow our functions to do different things each time they are called.
There is another way we can make our functions more flexible. We can pass some additional data to the function whenever we call it. These pieces of data are called the arguments of a function call.
Previously, we had a program like this:
#include <iostream>
using namespace std;
int Health{150};
void TakeDamage(){
cout << "Inflicting 50 Damage - ";
Health -= 50;
cout << "Health: " << Health << '\n';
}
int main(){
TakeDamage();
TakeDamage();
TakeDamage();
}
Inflicting 50 Damage - Health: 100
Inflicting 50 Damage - Health: 50
Inflicting 50 Damage - Health: 0
This function was quite inflexible. Every time we called it, it did 50
damage.
TakeDamage(); // 50 Damage
TakeDamage(); // 50 Damage
TakeDamage(); // 50 Damage
Instead, what we could do is pass some data into our function, letting the caller choose how much damage needs to be inflicted. That way, we could do a different amount of damage each time the function is called.
That value would be called an argument. We pass an argument between the (
and the )
that we've been using every time we call a function. For example:
TakeDamage(20); // 20 Damage
TakeDamage(50); // 50 Damage
TakeDamage(100); // 100 Damage
To make this work, we also need to update our function to let it accept arguments, and to make use of them.
When we call a function with an argument, that argument gets passed to a function parameter.
To allow our function to accept an argument, we need to set up a parameter to receive that argument.
We do that between the (
and )
at the top of the function. Similar to a variable, a parameter has a type and a name. The same naming rules and recommendations apply - we should give our parameters meaningful names.
Below, we give our function a parameter of the int
type, which we will call Damage
:
void TakeDamage(int Damage) {
Health -= 50;
}
The concepts of argument and parameters are very closely related, but are not quite the same thing.
Often, these are the same thing, but not always.
For example, we’ll later see how implicit conversions apply to this process, meaning a parameter can have a different type from the argument that it was created from.
There are also more differences we’ll become familiar with as the course progresses.
However, even though "argument" and "parameter" mean slightly different things in the strictest sense, they are also often used interchangeably when code is discussed informally. As such, we should also consider the context in which it's used.
Once we have set up our function to accept an argument, we can then update the body of the function to make use of that data where required. A parameter acts just the same as a variable within our function.
Instead of decreasing Health
by 50
, we can decrease it by whatever value is stored in the Damage
parameter:
void TakeDamage(int Damage) {
Health -= Damage;
}
Just like that, our code will now work. Our function can now be called with a different Damage
value each time:
#include <iostream>
using namespace std;
int Health{150};
void TakeDamage(int Damage){
cout << "Inflicting " << Damage << " Damage";
Health -= Damage;
cout << " - Health: " << Health << '\n';
}
int main(){
TakeDamage(20);
TakeDamage(50);
TakeDamage(70);
}
Inflicting 20 Damage - Health: 130
Inflicting 50 Damage - Health: 80
Inflicting 70 Damage - Health: 10
After running this code, what is the value of Result
?
int Square(int x) {
return x * x;
}
int Result { Square(2) };
By having multiple parameters in our parameter list, our function can receive multiple arguments. We separate multiple parameters using a comma ,
Below, our AddNumbers
function has three parameters, all of the int
type:
int AddNumbers(int x, int y, int z) {
return x + y + z;
}
When providing arguments for those parameters when we call our function, we also separate them with a comma:
// This will return 6
AddNumbers(1, 2, 3);
The parameters do not need to match the return type of the function, nor do they need to match the type of other parameters:
int Health { 100 };
void TakeDamage(int Damage, bool isMagical) {
// Magical damage is doubled
Health -= isMagical ? Damage * 2 : Damage;
}
TakeDamage(50, true);
If we try to pass an argument of the incorrect data type, the compiler will try to do the same implicit conversion technique we saw in previous lessons.
For example, if we try to pass an argument of 5
(an int
) into a parameter that has a type of float
, the parameter will be the floating point number 5.0
.
If we attempt to pass a data type that cannot be converted to what the parameter expects, the code will not compile, and we’ll be notified of the error.
// Float can be converted to int, so this
// will do 25 damage
TakeDamage(25.0f);
// Bool can be converted to int, so this
// will do 1 damage
TakeDamage(true);
// String cannot be converted to int, so
// this will be an error
TakeDamage("Hello!");
// This will also be an error - we have
// to provide something
TakeDamage();
After running this code, what is the value of Result
?
int Multiply(float x, float y) {
return x * y;
}
int Result { Multiply(2, 2.5) };
In our examples above, we added a second argument to the TakeDamage
function to denote whether the damage was magical.
void TakeDamage(int Damage, bool isMagical) {
// Magical damage is doubled
Health -= isMagical ? Damage * 2 : Damage;
}
This made our function more flexible but might make it a bit less intuitive to use. Often, we'll want our function to behave a certain way by default, but also to give callers the option to override that default behavior.
For scenarios like this, we can define optional parameters. For example, we can have our function assume that damage is not magical, but still let callers overrule that assumption if they need to:
// This will assumed to be non-magical damage
TakeDamage(50);
// This will override that assumption
TakeDamage(50, true);
To make a parameter optional, we just need to give it a default value in our parameter list. This is the value that the parameter will have if the caller opted not to provide it.
We do this using the =
operator within the parameter list. We’ve also moved our parameter list onto a new line. As with most things in C++, we’re free to lay out our code as desired:
void TakeDamage(
int Damage, bool isMagical = false
) {
// Magical damage is doubled
Health -= isMagical ? Damage * 2 : Damage;
}
With this change, callers of our function are free to provide or omit the additional argument as preferred
// This still works, but the false isn't needed
TakeDamage(50, false);
// It is equivalent to this:
TakeDamage(50);
// To override the default parameter:
TakeDamage(50, true);
We can have any number of optional parameters:
void TakeDamage(
int Damage,
bool isPhysicalDamage = true,
bool canBeLethalDamage = true,
) {
// body here
}
However, we cannot have the required parameters after the optional parameters.
We will see workarounds for this soon, but for now, just note that something like this would be impossible:
void TakeDamage(
int Damage,
bool isPhysicalDamage = true,
bool canBeLethalDamage
) {
// body here
}
To understand why optional parameters cannot come before required parameters, consider what would happen were we to call this function with an argument list like this:
TakeDamage(50, false);
What are we setting to false
here? Is it isPhysicalDamage
or canBeLethalDamage
? Our intent is not clear. So, the compiler does not allow our function to be set up that way in the first place.
After running this code, what is the value of Result
?
int Multiply(int x, int y = 3) {
return x * y;
}
int Result { Multiply(2) };
After running this code, what is the value of Result
?
int Multiply(int x = 2, int y) {
return x * y;
}
int Result { Multiply(2) };
We’ve seen how we can use a function’s default parameter simply by omitting the argument in the corresponding position.
Alternatively, we can explicitly use a default parameter by passing an empty set of braces, {}
, in that position.
int Add(int x = 1, int y = 2, int z = 3){
return x + y + z;
}
int main(){
// Will return 6
Add({}, {}, {});
}
This is required if we want to use default parameters in early positions, but provide a value for a later parameter. Below, we use the function’s default values for parameters x
and y
, but provide a custom value for z
:
int Add(int x = 1, int y = 2, int z = 3){
return x + y + z;
}
int main(){
// Will return 8
Add({}, {}, 5);
}
In the previous examples, all our arguments were passed as "literal" values, for example, 25
and true
.
In reality, we can use any expression that results in a value of the correct type for the respective parameter, or one that can be converted to that type. For example:
bool isWizard { true };
// This will be equivalent to TakeDamage(50, true)
TakeDamage(25 * 2, isWizard);
Our arguments can also be determined by calls to other functions. As long as that other function returns something of an appropriate type, we can use it:
int CalculateDamage() {
return 25;
};
TakeDamage(CalculateDamage(), true);
We can even compose functions with themselves:
// Will become AddNumbers(1, 2, 12)
// Which will become 15
AddNumbers(1, 2, AddNumbers(3, 4, 5));
After running this code, what is the value of Result
?
int Multiply(int x, int y = 3) {
return x * y;
}
int Result { Multiply(2, Multiply(5)) };
Frequently, the value we want to use in an argument of a function is simply stored in a variable:
int DamageToInflict { 50 };
void TakeDamage(int Damage) {
// Do stuff
}
TakeDamage(DamageToInflict)
This example is often a point of confusion, as the names do not match up. We created a variable called DamageToInflict
and passed it to a function. But the name within the function was different - it was called Damage
.
What we call the variable outside the function does not correlate with what we call the parameter. Whilst they might contain the same value, they are two different variables.
Arguments are not mapped to parameters by their name, rather, they are mapped by their order.
When we call a function, the first argument maps to the first parameter; the second argument maps to the second parameter, and so on.
We can see this in the following example:
#include <iostream>
using namespace std;
int x{5};
int y{2};
void SubtractNumbers(int a, int b){
cout << "\na = " << a
<< "\nb = " << b
<< "\nResult = " << a - b;
}
int main(){
cout << "Calculating " << x << " - " << y;
SubtractNumbers(x, y);
cout << "\n\nCalculating " << y << " - " << x;
SubtractNumbers(y, x);
}
Calculating 5 - 2
a = 5
b = 2
Result = 3
Calculating 2 - 5
a = 2
b = 5
Result = -3
If these results make sense, we can make things slightly more complicated to solidify the point. Let's rename our function parameters to also be called x
and y
:
#include <iostream>
using namespace std;
int x{5};
int y{2};
void SubtractNumbers(int x, int y){
cout << "\nx = " << x
<< "\ny = " << y
<< "\nResult = " << x - y;
}
int main(){
cout << "Calculating " << x << " - " << y;
SubtractNumbers(x, y);
cout << "\n\nCalculating " << y << " - " << x;
SubtractNumbers(y, x);
}
We now have variables called x
and y
both outside and inside the function. However, our results are the same:
Calculating 5 - 2
x = 5
y = 2
Result = 3
Calculating 2 - 5
x = 2
y = 5
Result = -3
This is because they're different variables - the x
and y
outside of our function are not the same as the x
and y
within our function. They just have the same name. The next lesson in the course is focused on scopes, which discusses this situation in much more detail.
After running this code, what is the value of Result
?
int x { 1 };
int y { 3 };
int Subtract(int x, int y) {
return x - y;
}
int Result { Subtract(y, x) };
This concept is quite confusing, so don't worry if this section didn't make sense.
The next lesson explores this topic in more detail and will help build our understanding.
In this lesson, we explored the fundamental concepts of function arguments and parameters in C++, enhancing our understanding of how to make functions more dynamic and versatile.
Here's a recap of what we've learned:
In our upcoming lesson, we delve into the concepts of Global and Local Scope.
You'll learn about the scope of variables, how it affects their accessibility within different parts of a program.
Making our functions more useful and dynamic by providing them with additional values to use in their execution
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way