Functions
A function is a named, independent section of code that performs a specific task and optionally returns a computed value. Variable scope refers to the part of a program where a particular variable is declared and can be used.
Table of Contents
- Functions
- Function Definition and Execution
- Function Parameters and Arguments
- Return Values
- Whitespace
- Forward Declaration
- Functions and Header Files
- What to Return from Main
- Variable Scope
- Static Local Variables
- Pass by Value
- Pass By Const Reference
- Pass by Reference
- In-Out Parameters
- Multiple Out Parameters
- Function Overloading
- Returning Different Types
- Templates
- Resources
Functions
Functions are the verbs of computer programming. They do things.
They increase:
- Modularity, by allowing us to break down large programs into smaller parts.
- Reusability, by allowing us to reuse code without having to retype or copy/paste it.
- Readability, by allowing us to give descriptive names to specific parts of our program.
Function Definition and Execution
Functions are defined in C++ with a return type, a name, and an optional list of input parameters.
type functionName(type parameter1, type parameter2) {
// function body
}
A function with a return type of void
does not return a value.
void sayGoodnight() {
std::cout << "So Long\nFarewell\n";
}
// To execute this function:
sayGoodnight();
Function Parameters and Arguments
Functions can be defined to take one or more arguments by way of a parameter list.
Default values for the parameters can also be specified.
void sayGoodnightRepeatedly(std::string name, int numberOfTimes = 1) {
for (int i{0}; i < numberOfTimes; i++) {
std::cout << "Goodnight " << name << "\n";
}
}
sayGoodnightRepeatedly("Wally", 12);
sayGoodnightRepeatedly("Wally"); // Second argument defaults to 1.
Return Values
Functions can optionally return a value to the caller.
std::string pizzaMessage(int piecesLeft, int hungryPeople) {
if (piecesLeft < hungryPeople) {
return "Sorry we don't have enough pizza.";
} else {
return "Let's share! Any leftovers go to the dog.";
}
}
std::cout << pizzaMessage(13, 7) << "\n";
Whitespace
Because C++ ignores whitespace you’ll sometimes see function calls spread over multiple lines.
This is done for readability purposes and helps avoid extra-long lines of code.
functionName(argumentOne,
argumentTwo,
argumentThree,
argumentFour);
Forward Declaration
Functions need to be declared before they can be used.
Try to compile/run this program:
#include <iostream> // for std::cout & std::cin
#include <limits> // for std::numeric_limits
#include <string> // for std::string
// Uncomment this line to "forward declare" the fetchInteger function:
// int fetchInteger(std::string prompt);
int main() {
// Won't compile because fetchInteger hasn't been declared yet.
int number = fetchInteger("What's your favourite number? ");
std::cout << number << "\n";
}
int fetchInteger(std::string prompt) {
do {
int number;
std::cout << prompt;
if (std::cin >> number) {
return number;
}
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
} while (true);
}
Instead of moving the fetchInteger
function above the main
function, we can forward declare fetchInteger
. Uncomment the forward declaration to fix the above code.ss
🎵 Note:
Parameter names are optional when forward declaring a function.
In the above program the forward declaration could have been:
int fetchInteger(std::string);
Functions and Header Files
It’s common to forward declare functions defined in separate .cpp
source files using .h
header files.
First the function is forward declared in a header with include guards:
userInput.h
:
#pragma once
// Forward declaration with no implementation.
int fetchInteger(std::string prompt);
The function can now be implemented in a .cpp
file separate from where main()
is defined. Both .cpp
files must then #include
the associated .h
header file:
🚀 Run this example on Compiler Explorer!
What to Return from Main
The main()
function is the only function with a non-void
type where we don’t have to explicitly return
:
int main() {
int goatCount{ 12 };
}
The main()
function is your program’s entry point. The value returned by main()
is called a status code or exit code, and it’s sent to the Operating System when the program exits.
Non-zero status codes are used to indicate program failure.
If no return
is present it’s equivalent to returning 0
, meaning success.
The <cstdlib>
header defines two status codes you can use when returning from main()
:
EXIT_SUCCESS
EXIT_FAILURE
Variable Scope
Like other {}
blocks, C++ functions each have their own scope.
std::string gemFactory(int numberOfGems) {
// The gems variable is a local variable.
std::string gems = std::string(numberOfGems, '💎'); // gems variable is scoped to function.
return gems;
}
int main() {
std::cout << gemFactory(5) << "\n";
std::cout << gems; // Error: "gems" variable is out of scope.
}
Static Local Variables
We can extend the scope of a local variable in a function, such that it persists between function calls, by using the static
keyword.
int countingCrow() {
static int count{ 0 };
return ++count;
}
std::cout << countingCrow() << "\n"; // Prints: 1
std::cout << countingCrow() << "\n"; // Prints: 2
std::cout << countingCrow() << "\n"; // Prints: 3
Pass by Value
C++ functions are pass-by-value by default, meaning copies are made of the arguments passed to a function.
int increment(int number) {
number++;
return number;
}
void main() {
int a{ 5 };
int b{ increment(a) }; // The value of "a" is copied to the "number" parameter.
std::cout << a << "\n"; // Remained 5
std::cout << b << "\n"; // 6
}
🎵 Note:
There is a performance “copy cost” for non-primitive types like arrays or objects.
Pass By Const Reference
To avoid the performance cost associated with pass-by-value we use const references for our function parameters. Passing a reference to a variable means no copy needs to be performed, the const
prevents the function from changing the referenced variable.
For example, let’s use a reference parameter to avoid the cost of copying a large object into a function.
void logMonster(const Monster& m) {
// Implementation details are unimportant.
}
☝️ Code assumes the Monster
class is defined elsewhere.
Pass by Reference
Changes made to non-const
reference parameters affect the variable passed into the function.
int mutator(int& number) {
number++; // Changes the value of the variable referenced by "number".
return number;
}
void main() {
int a{ 5 };
int b{ mutator(a) }; // "a" and "b" become 6.
std::cout << a << "\n"; // 6
std::cout << b << "\n"; // 6
}
💡 Best Practice:
Prefer return
statements to using references to return data from functions.
In-Out Parameters
Keeping the above best practice in mind, the performance hit of copying large variables into and then out of a function can be avoided using in-out-parameters.
Non-const
reference parameters allow expensive to copy variables to be modified by a function:
void update(Monster& monster) {
// Updates the object referenced by "monster".
// Implementation unimportant.
}
Multiple Out Parameters
Resist the urge to use reference parameters as a way of returning multiple values from a function.
void calculateMovement(int time, int& xPosition, int& yPosition) {
// Implementation unimportant.
}
⏳ Wait For It:
It’s better to return a composite data type like a struct
, tuple
, pair
, etc.
Function Overloading
We can create different versions of the same named function that:
- Have different data types for the arguments.
- Have different numbers of arguments.
Imagine a debugging function called debugFormat()
defined in a few different ways:
void debugFormat() {
std::cout << "DEBUG\n";
}
void debugFormat(int number) {
std::cout << "DEBUG (int): " << number << "\n";
}
void debugFormat(double number) {
std::cout << "DEBUG (double): " << number << "\n";
}
void debugFormat(std::string label, double number) {
std::cout << label << " (double): " << number << "\n";
}
debugFormat(); // Outputs: DEBUG
debugFormat(42); // Outputs: DEBUG (int): 42
debugFormat(3.14); // Outputs: DEBUG (double): 3.14
debugFormat("WARNING", 3.14); // Outputs: WARNING (double): 3.14
Returning Different Types
There may also be instances when you overload a function with a different number/type of parameters and also change the return type.
int max(int a, int b) {
return (a > b) ? a : b;
}
double max(double a, double b) {
return (a > b) ? a : b;
}
🎵 Note:
Overloaded functions with different return types need not have identical function bodies.
⚡ Warning:
We cannot overload a function based only on a change in return type.
Templates
Templates in C++ allow you to write generic functions that can work with any data type, making your code more flexible and reusable. While templates can be complex, we’ll start with a simple example to demonstrate their basic concept without exploring the more advanced features.
In the previous section we defined a max
function that returns the larger of two variable values. To support both ints and doubles we had to write two separate implementations. Doesn’t seem very DRY.
What if we could define a generic version of the function that would work for all data types that support comparisons with the >
operator?
#include <iostream>
#include <string>
// A templated max function where T can be any one type.
template <typename T>
T myMax(T a, T b) {
return (a > b) ? a : b;
}
int main() {
int int1 = 3, int2 = 5;
double double1 = 7.5, double2 = 2.3;
std::string str1 = "apple", str2 = "orange";
std::cout << "Max of " << int1 << " and " << int2 << " is: " << myMax(int1, int2) << std::endl;
std::cout << "Max of " << double1 << " and " << double2 << " is: " << myMax(double1, double2) << std::endl;
std::cout << "Max of \"" << str1 << "\" and \"" << str2 << "\" is: \"" << myMax(str1, str2) << "\"" << std::endl;
return 0;
}