Functions and Scope

A quick guide to functions in C++, and how global and local scopes interact
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

a.jpg
Ryan McCombe
Ryan McCombe
Posted

This lesson is a quick introductory tour of functions within C++. It is not intended for those who are entirely new to programming. Rather, the people who may find it useful include:

  • those who have completed our introductory course, but want a quick review
  • those who are already familiar with programming in another language, but new to C++
  • those who have used C++ in the past, but would benefit from a refresher

It summarises several lessons from our introductory course. Anyone looking for more thorough explanations or additional context should consider Chapter 2 of that course.

Previous Course

Intro to Programming with C++

Starting from the fundamentals, become a C++ software engineer, step by step.

Screenshot from Cyberpunk 2077
Screenshot from The Witcher 3: Wild Hunt

Function Syntax

In C++, a function that is called MyFunction that returns void (ie, nothing) and does nothing looks like this:

void MyFunction() {}

Here are examples of functions that return data. Note that we specify the return type in the function heading:

float ReturnFloat() {
  return 5.5f;
}

bool ReturnBool() {
  return true;
}

Similar to variables, return statements can be implicitly cast to the return type of the function:

// This returns the integer 5
int ReturnInt() {
  return 5.5f;
}

We invoke functions using their name, and ():

ReturnFloat();

The main Function

Our program needs to define an entry point - the function that will be run when our program is executed.

This is typically a function that returns an int and is called main:

int main() {
  return 0;
}

The return value of the main function is an exit code, which describes why our program stopped. Returning 0 indicates the program ran as expected. Any other number is an error code.

If the main function returns nothing, it is assumed to have returned 0. This is a behavior that is specific just to the main function. Every other function must return something of the type specified in its heading, or something that can be converted to that type.

Function Parameters

We define parameters between the () in the function specification. We specify their type and name, and separate multiple parameters using commas:

int Add(int x, int y) {
  return x + y;
}

int main() {
  Add(1, 2); // 3
}

We can define default values for parameters, making them optional. We can pass {} as an argument to use the default value in that position, or omit the arguments entirely if they are at the end:

int Add(int x = 1, int y = 2) {
  return x + y;
}

int main() {
  // Use no defaults
  Add(2, 3); // 5

  // Use default for 1st parameter
  Add({}, 2); // 3
  
  // Use default for 2nd parameter
  Add(2, {}); // 4
  Add(2); // 4

  // Use default for both parameters
  Add({}, {}); // 3
  Add(); // 3
}

Function return types can be set to auto which will ask the compiler to infer what it should be based on the return statements:

auto Add(int x, int y) {
  return x + y;
}

Function parameters can also have a type of auto, but this doesn’t mean what we might expect. It creates a function template, which is explained later in this course.

Implicit Argument Conversion

Like with variables, the compiler will attempt to implicitly convert function arguments and return values into the appropriate type. It will only present an error if it cannot do the conversion.

// Will return 2
int GetInt() {
  return 2.5f;
}

int Add(int x, int y) {
  return x + y;
}
// All expressions will return 2
// they are equivalent to Add(2, 2)
Add(2, 2);
Add(2.5f, 2.5f);
Add(2, 2.5f);
Add(2.5, 2.5);

Function Overloading

It’s possible to have two functions with the same name in the same scope.

The way the compiler determines which function we want to call is by comparing the argument types to the parameter types:

// Integer Overload
int Add(int x, int y) {
  return x + y;
}

// Float Overload
float Add(float x, float y) {
  return x + y;
}

Add(2, 2); // Calling Integer Overload
Add(2.5f, 2.5f); // Calling Float Overload

If it is ambiguous what function we want to call, the compiler will throw an error. The following is ambiguous because the compiler doesn’t know whether it should convert the first argument or the second argument:

Add(2, 2.5f); 

It could convert the first to a float, and call the overloads that accept a (float, float) argument list. Or, it could convert the second argument to an int, and call the (float, float) overload.

It doesn’t know what we want, so it throws an error.

The following also throws an error, as we provide two double arguments. A double can be converted to either a float or an int so again, we’re being ambiguous:

Add(2.5, 2.5); 

We can remove the ambiguity by explicitly casting our arguments to the desired type.

// Calling (int, int) Overload
Add(2, static_cast<int>(2.5f));

// Calling (float, float) Overload
Add(
  static_cast<float>(2.5),
  static_cast<float>(2.5)
);

Forward Declaration

Functions can be declared and defined as separate statements. A function declaration looks the same as a definition, but without the body:

int Add(int x, int y);

A function’s return type, name, and parameter types are often called the function prototype or function signature.

Function prototypes technically don’t need to name their parameters:

int Add(int, int);

But it’s common to include the names, to remind ourselves (or other developers reading our code) what the parameters are for.

Function declarations allow our functions to be called even if they are not yet defined at the place where they are called.

This is referred to as a forward declaration:

int Add(int x, int y);

int main() {
  Add(1, 2);
}

int Add(int x, int y = 1) {
  return x + y;
}

The difference between a declaration and a definition will become more important as we advance through this course.

Global and Local Scope

Functions and any other block delimited by { and } create a new local scope. Expressions can access their own scope, and any ancestor (parent, grandparent, etc) scope. But they cannot access any descendant (child, grandchild, etc) scopes:

// Global scope
int GlobalInt{0};

void MyFunction() {
  // Local scope
  int LocalInt{0};

  // This is fine - we're accessing
  // a variable in the parent scope
  GlobalInt++;
}

// This is fine - same scope
++GlobalInt;

// Not fine - we do not have access
// to MyFunction's scope from here
++LocalInt;

Was this lesson useful?

Edit History

  • — First Published

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

7a.jpg
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access!

This course includes:

  • 106 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Namespaces, Includes, and the Standard Library

A quick introduction to namespaces in C++, alongside the standard library and how we can access it
14.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved