본문으로 건너뛰기

Functions

Function: A named block of code that performs a specific task.

Function Call: Transfers control to a function and executes its body.

Function Overloading: Allows multiple functions with the same name to perform related operations on different parameter types.

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

int result = add(10, 20);

The function:

add

receives two arguments and returns their sum.


6.1 Function Basics

A function definition consists of:

  1. return type
  2. function name
  3. parameter list
  4. function body
int add(int a, int b)
{
return a + b;
}

Here:

int return type
add function name
int a, int b parameters
{ ... } function body

Parameter

Parameter: A variable declared in a function's parameter list.

int square(int value)
{
return value * value;
}

value is a parameter.

Argument

Argument: A value supplied by the caller to initialize a parameter.

int result = square(5);

5 is the argument.

The call initializes:

value = 5

Writing a Function

A function can perform calculations and return a result.

int absolute_value(int value)
{
if (value < 0)
{
return -value;
}

return value;
}

Usage:

int result = absolute_value(-10);

Result:

10

Calling a Function

When a function is called:

  1. arguments are evaluated
  2. parameters are initialized
  3. control transfers to the function
  4. the function body executes
  5. return transfers control back to the caller
int multiply(int a, int b)
{
return a * b;
}

int result = multiply(4, 5);

Conceptually:

a = 4
b = 5

a * b = 20

return 20

Parameters and Arguments

Each argument corresponds to one parameter.

int subtract(int a, int b)
{
return a - b;
}

int result = subtract(10, 3);

The mapping is:

10 -> a
3 -> b

Result:

7

The argument must have a type that matches or can be converted to the parameter type.

double square(double value)
{
return value * value;
}

double result = square(5);

The integer 5 is converted to double.

Do not write code whose result depends on the evaluation order of function arguments.


Function Parameter List

Parameters are separated by commas.

int calculate(int a, int b, int c)
{
return a + b + c;
}

A function may have no parameters.

void print_message()
{
std::cout << "Hello\n";
}

Call:

print_message();

An empty parameter list should normally be written:

()

Function Return Type

A function returning a value specifies that value's type.

double average(double a, double b)
{
return (a + b) / 2.0;
}

A function that returns nothing uses void.

void print_value(int value)
{
std::cout << value << '\n';
}

A function cannot directly return an array or function type.

It can return:

  • an object
  • a reference
  • a pointer
  • a pointer to an array
  • a pointer to a function

1) Local Objects

Local Object: An object defined inside a function or block.

int calculate()
{
int value = 10;

return value * 2;
}

value is visible only inside calculate.


Scope vs. Lifetime

Scope: Where a name can be used in source code.

Lifetime: How long an object exists during program execution.

void function()
{
int value = 10;
}

value:

scope = function body
lifetime = while the function call is active

Automatic Objects

Most local variables are automatic objects.

void function()
{
int value = 10;
}

value is created when execution reaches its definition.

It is destroyed when execution leaves the block.

Parameters are also automatic objects.

void print(int value)
{
std::cout << value;
}

value exists only during the call to print.

Local built-in variables should be initialized before they are read.

Prefer:

int value = 0;

over:

int value;

when an initial value is available.


Local static Objects

Local static Object: A local variable whose lifetime lasts until program termination.

std::size_t count_calls()
{
static std::size_t count = 0;

return ++count;
}

Calls:

std::cout << count_calls() << '\n';
std::cout << count_calls() << '\n';
std::cout << count_calls() << '\n';

Output:

1
2
3

Unlike an ordinary local variable, count keeps its value between calls.

Conceptually:

first call -> count = 1
second call -> count = 2
third call -> count = 3

2) Function Declarations

Function Declaration: Makes a function's name, return type, and parameter types known before the function is used.

int add(int, int);

The function can then be called:

int result = add(10, 20);

and defined later:

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

A function may be declared many times but normally has one definition.


Parameter Names in Declarations

Parameter names are optional in declarations.

These declarations are equivalent:

int add(int a, int b);
int add(int, int);

Names are usually useful when they document the meaning of each parameter.

double calculate_area(
double width,
double height
);

Function Declarations in Header Files

Shared function declarations normally belong in a header.

// math.hpp

#pragma once

int add(int a, int b);
int subtract(int a, int b);

Definitions go in a source file.

// math.cpp

#include "math.hpp"

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

int subtract(int a, int b)
{
return a - b;
}

Application:

// main.cpp

#include <iostream>

#include "math.hpp"

int main()
{
std::cout << add(10, 20) << '\n';
}

The source file defining a function should include the header containing its declaration.

This helps the compiler detect mismatches between the declaration and definition.


3) Separate Compilation

Separate Compilation: Allows different source files to be compiled independently.

Example structure:

project/
├─ main.cpp
├─ math.cpp
└─ math.hpp

Compilation conceptually produces:

main.cpp

main.obj

math.cpp

math.obj

The linker then combines them:

main.obj
math.obj

linker

program.exe

This allows only changed files to be recompiled in large projects.


6.2 Argument Passing

Function parameters are initialized from arguments.

There are two important forms:

pass by value
pass by reference

1) Passing Arguments by Value

Pass by Value: Copies the argument's value into the parameter.

void change(int value)
{
value = 100;
}

Usage:

int number = 10;

change(number);

After the call:

number = 10

The parameter and argument are separate objects.

Conceptually:

number = 10

copy


value = 10

Changing value does not change number.


Pointer Parameters

Pointers are also passed by value.

void reset(int* pointer)
{
*pointer = 0;
}

Usage:

int value = 10;

reset(&value);

After the call:

value = 0

The pointer itself was copied, but both pointer values refer to the same object.

argument pointer ──┐
├──> value
parameter pointer ─┘

Reassigning a Pointer Parameter

Changing the copied pointer itself does not change the caller's pointer.

void reset_pointer(int* pointer)
{
pointer = nullptr;
}

Usage:

int value = 10;

int* p = &value;

reset_pointer(p);

After the call:

p == &value

The original pointer p is unchanged.


2) Passing Arguments by Reference

Reference Parameter: Acts as an alias for the caller's object.

void reset(int& value)
{
value = 0;
}

Usage:

int number = 10;

reset(number);

Afterward:

number = 0

The parameter and argument refer to the same object.

number


value reference

Swapping with References

References make it easy to modify multiple caller objects.

void swap_values(int& a, int& b)
{
int temp = a;

a = b;
b = temp;
}

Usage:

int x = 10;
int y = 20;

swap_values(x, y);

Result:

x = 20
y = 10

Using References to Avoid Copies

Large objects should often be passed by reference instead of copied.

Less efficient:

std::size_t length(std::string text)
{
return text.size();
}

This copies the string.

Prefer:

std::size_t length(const std::string& text)
{
return text.size();
}

The function:

  • does not copy the string
  • cannot modify it through text

Reference to const

Use const T& when a function needs only to read an object.

bool is_empty(const std::string& text)
{
return text.empty();
}

Usage:

std::string name = "Grap";

bool result = is_empty(name);

A const reference can also accept temporary values.

is_empty("hello");

Reference Parameters as Additional Results

A function normally returns one value.

Reference parameters can provide additional output.

std::string::size_type find_char(
const std::string& text,
char target,
std::string::size_type& occurrences)
{
auto first_position = text.size();

occurrences = 0;

for (std::string::size_type i = 0;
i != text.size();
++i)
{
if (text[i] == target)
{
if (first_position == text.size())
{
first_position = i;
}

++occurrences;
}
}

return first_position;
}

Usage:

std::string text = "hello";

std::string::size_type count = 0;

auto position =
find_char(text, 'l', count);

Results:

position = 2
count = 2

The function returns one value normally and another through count.


3) const Parameters and Arguments

Top-Level const

For pass-by-value parameters, top-level const does not create a different function type.

These cannot be overloaded separately:

void function(int);
// Same parameter type for overloading purposes
void function(const int);

The parameter is still a copied int.

Inside the second function, the local copy cannot be modified, but callers see the same interface.


Low-Level const

Low-level const does affect reference and pointer parameter types.

These are different:

void process(int& value);
void process(const int& value);

Likewise:

void process(int* value);
void process(const int* value);

Reference Binding

A non-const reference requires a compatible modifiable object.

void modify(int& value)
{
++value;
}

Valid:

int value = 10;

modify(value);

Invalid:

// modify(10);

because a non-const lvalue reference cannot bind to the literal.

A reference to const is more flexible:

void inspect(const int& value)
{
std::cout << value;
}

Valid:

int value = 10;

inspect(value);
inspect(10);

Use Reference to const When Possible

If a function does not modify an object, prefer:

void print(const std::string& text)
{
std::cout << text;
}

instead of:

void print(std::string& text)
{
std::cout << text;
}

The second form unnecessarily prevents calls with:

  • const strings
  • temporary strings
  • compatible temporary values

4) Array Parameters

When an array is passed to a function, it normally converts to a pointer to its first element.

These parameter declarations are effectively equivalent:

void print(const int* values);
void print(const int values[]);
void print(const int values[10]);

The 10 does not make the function receive an entire ten-element array by value.

The function receives a pointer.


Array Size Is Lost

void print(const int values[])
{
// sizeof(values) is the size of a pointer,
// not the original array
}

Therefore the function needs another way to determine the array's extent.

Common approaches are:

  1. end marker
  2. begin/end pointers
  3. explicit size
  4. reference to array

End Marker

C-style strings use a null terminator.

void print(const char* text)
{
while (*text)
{
std::cout << *text;

++text;
}
}

Usage:

print("Hello");

The final '\0' marks the end.


Begin-and-End Range

A common C++ convention passes the first element and one-past-the-last element.

void print(
const int* begin,
const int* end)
{
while (begin != end)
{
std::cout << *begin << '\n';

++begin;
}
}

Usage:

#include <iterator>

int values[] = {10, 20, 30};

print(
std::begin(values),
std::end(values)
);

This represents the half-open range:

[begin, end)

The element at end itself is not accessed.


Explicit Size Parameter

Another approach is to pass the number of elements.

void print(
const int values[],
std::size_t size)
{
for (std::size_t i = 0;
i != size;
++i)
{
std::cout << values[i] << '\n';
}
}

Usage:

int values[] = {10, 20, 30};

print(values, 3);

Array Parameters and const

Use:

const int*

when elements should not be modified.

void print(const int values[], std::size_t size);

Use:

int*

when modification is required.

void clear(int values[], std::size_t size)
{
for (std::size_t i = 0;
i != size;
++i)
{
values[i] = 0;
}
}

Reference to Array

A reference parameter can preserve the array dimension.

void print(int (&values)[10])
{
for (auto value : values)
{
std::cout << value << '\n';
}
}

This function accepts only arrays of exactly ten ints.

int a[10];

print(a);

But:

int b[5];

// Error:
// print(b);

because the dimension is part of the reference type.


Array Reference Template

Modern generic code can deduce the dimension.

template <std::size_t N>
void print(const int (&values)[N])
{
for (auto value : values)
{
std::cout << value << '\n';
}
}

Now arrays of different sizes can be accepted without losing their dimensions.


Passing a Multidimensional Array

Consider:

int matrix[3][4];

A parameter may be written:

void print(int (*matrix)[4], std::size_t rows);

or equivalently:

void print(int matrix[][4], std::size_t rows);

The inner dimension must be known.

Example:

void print(
const int matrix[][4],
std::size_t rows)
{
for (std::size_t row = 0;
row != rows;
++row)
{
for (std::size_t column = 0;
column != 4;
++column)
{
std::cout
<< matrix[row][column]
<< ' ';
}

std::cout << '\n';
}
}

5) main: Handling Command-Line Options

main can receive command-line arguments.

int main(int argc, char* argv[])
{
// ...
}

Equivalent form:

int main(int argc, char** argv)
{
// ...
}

argc

argc: Number of command-line argument strings.

argv

argv: Array of pointers to C-style strings containing the arguments.

Suppose the program is executed as:

program -o output.txt data.txt

Then conceptually:

argc = 4

argv[0] = "program"
argv[1] = "-o"
argv[2] = "output.txt"
argv[3] = "data.txt"
argv[4] = nullptr

User-provided arguments begin at:

argv[1]

not argv[0].


Printing Command-Line Arguments

#include <iostream>

int main(int argc, char* argv[])
{
for (int i = 0; i != argc; ++i)
{
std::cout
<< argv[i]
<< '\n';
}
}

Converting an Argument to std::string

#include <string>

int main(int argc, char* argv[])
{
if (argc > 1)
{
std::string first_argument =
argv[1];
}
}

6) Functions with Varying Parameters

Sometimes a function must accept a varying number of arguments.

Important approaches include:

initializer_list
variadic templates
ellipsis (...)

If all varying arguments have the same type, initializer_list is useful.


initializer_list

Required header:

#include <initializer_list>

Example:

int sum(std::initializer_list<int> values)
{
int result = 0;

for (int value : values)
{
result += value;
}

return result;
}

Usage:

int result =
sum({1, 2, 3, 4, 5});

Result:

15

initializer_list Operations

OperationMeaning
initializer_list<T> listEmpty list
initializer_list<T>{...}Create list
list.size()Number of elements
list.begin()First element
list.end()One past last element

The elements themselves are const.

void print(
std::initializer_list<std::string> values)
{
for (const auto& value : values)
{
std::cout << value << '\n';
}
}

Usage:

print({
"error",
"invalid file",
"data.txt"
});

Fixed and Varying Parameters Together

A function may combine ordinary parameters with initializer_list.

void error(
int error_code,
std::initializer_list<std::string> messages)
{
std::cerr
<< "error "
<< error_code
<< '\n';

for (const auto& message : messages)
{
std::cerr
<< message
<< '\n';
}
}

Usage:

error(
10,
{"file not found", "data.txt"}
);

Ellipsis Parameters

An ellipsis accepts additional arguments.

void legacy_function(int count, ...);

However, the additional arguments do not receive normal C++ type checking.

This mechanism mainly exists for compatibility with C-style variable-argument functions.

For normal modern C++ code, prefer:

  • initializer_list for arguments of one type
  • variadic templates for differing types

6.3 Return Types and the return Statement

return: Terminates a function and transfers control to the caller.

Two basic forms:

return;

and:

return expression;

1) Functions with No Return Value

A void function does not return a value.

void print(int value)
{
std::cout << value << '\n';
}

Reaching the end automatically returns to the caller.


Early Return

A void function may explicitly return early.

void print_positive(int value)
{
if (value <= 0)
{
return;
}

std::cout << value << '\n';
}

If value <= 0, the output statement is skipped.


2) Functions That Return a Value

A non-void function returns a value compatible with its return type.

int square(int value)
{
return value * value;
}

Usage:

int result = square(5);

Result:

25

Return Conversion

The returned expression can be converted to the return type.

int function()
{
return 3.14;
}

The returned value becomes approximately:

3

because the function returns int.

Such conversions should be intentional.


Return by Value

std::string make_name()
{
std::string name = "Grap";

return name;
}

Usage:

std::string result = make_name();

The caller receives a string result.

Modern C++ compilers can efficiently handle such returns using copy elision and moves.


Return by Reference

Returning a reference avoids copying and refers to an existing object.

const std::string& shorter_string(
const std::string& a,
const std::string& b)
{
return a.size() <= b.size()
? a
: b;
}

Usage:

std::string a = "hello";
std::string b = "hi";

const std::string& result =
shorter_string(a, b);

result refers directly to b.


Never Return a Reference to a Local Object

Wrong:

const std::string& bad()
{
std::string text = "hello";

return text;
}

text is destroyed when the function returns.

The returned reference becomes invalid.

Likewise, never return a pointer to a local object.

Wrong:

int* bad()
{
int value = 10;

return &value;
}

After the function ends, value no longer exists.


Safe Reference Return

A reference may safely refer to an object whose lifetime continues after the function returns.

int& first(std::vector<int>& values)
{
return values[0];
}

Usage:

std::vector<int> values{10, 20, 30};

first(values) = 100;

Now:

values = {100, 20, 30}

A function returning a non-const reference produces an lvalue.


Reference to const Return

const int& first(
const std::vector<int>& values)
{
return values[0];
}

The returned reference can be read:

std::cout << first(values);

but not modified through the returned expression.


Member Access on a Return Value

A function returning a class object can be used directly.

std::string make_name()
{
return "Grap";
}

auto length =
make_name().size();

The returned std::string immediately calls its size() member.


List-Initialized Return Value

A function returning a container can use a braced return.

std::vector<std::string>
process()
{
return {
"one",
"two",
"three"
};
}

An empty result can be written:

return {};

Return from main

Reaching the end of main implicitly returns 0.

int main()
{
}

is conceptually equivalent to:

int main()
{
return 0;
}

The <cstdlib> header also defines:

EXIT_SUCCESS
EXIT_FAILURE

Example:

#include <cstdlib>

int main()
{
if (failed())
{
return EXIT_FAILURE;
}

return EXIT_SUCCESS;
}

Recursion

Recursive Function: A function that calls itself.

Example: factorial.

Mathematically:

n!=n(n1)!n! = n(n-1)!

with:

0!=10! = 1

Implementation:

int factorial(int value)
{
if (value <= 1)
{
return 1;
}

return value * factorial(value - 1);
}

Usage:

int result = factorial(5);

Evaluation:

factorial(5)
5 * factorial(4)
5 * 4 * factorial(3)
5 * 4 * 3 * factorial(2)
5 * 4 * 3 * 2 * factorial(1)

Result:

120

A recursive function needs a stopping condition.

Without one, calls continue until resources such as the call stack are exhausted.

main may not recursively call itself.


3) Returning a Pointer to an Array

A function cannot return an array directly.

It can return a pointer to an array.

Consider:

int array1[10];
int array2[10];

Type Alias

A type alias simplifies the declaration.

using Array10 = int[10];

A function returning a pointer to such an array:

Array10* select_array(int index)
{
return index == 0
? &array1
: &array2;
}

Direct Pointer-to-Array Syntax

Without an alias:

int (*select_array(int index))[10]
{
return index == 0
? &array1
: &array2;
}

Read from the function name outward:

select_array
function taking int

*select_array(...)
returns a pointer

(*select_array(...))[10]
pointer to array of 10

int
array elements are int

Trailing Return Type

The same declaration is easier to read with a trailing return type.

auto select_array(int index)
-> int (*)[10]
{
return index == 0
? &array1
: &array2;
}

Using decltype

Given an existing array:

int values[10];

decltype(values) is:

int[10]

Therefore:

decltype(values)* get_values()
{
return &values;
}

The * is required because decltype(values) itself is the array type.


6.4 Overloaded Functions

Function Overloading: Multiple functions with the same name but different parameter lists.

void print(int value)
{
std::cout << value;
}

void print(double value)
{
std::cout << value;
}

void print(const std::string& value)
{
std::cout << value;
}

Calls:

print(10);
print(3.14);
print("hello");

The compiler selects the appropriate overload from the arguments.


Different Parameter Counts

Overloads may differ in the number of parameters.

int sum(int a)
{
return a;
}

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

int sum(int a, int b, int c)
{
return a + b + c;
}

Different Parameter Types

void process(int value);
void process(double value);

These are valid overloads.


Return Type Alone Is Not Enough

Invalid:

int function(int value);

// Error:
// double function(int value);

The parameter lists are identical.

The compiler cannot choose an overload based on the type expected by the caller.


Parameter Names Do Not Matter

These declare the same function:

void function(int value);
void function(int number);

Parameter names are not part of the function type.


Type Aliases Do Not Create New Types

using Integer = int;

void function(int);

This does not create a different overload:

// Same type:
// void function(Integer);

because Integer is another name for int.


Top-Level const Does Not Distinguish Overloads

These represent the same parameter type for overloading:

void function(int);
// Same overload:
// void function(const int);

Likewise for a copied pointer:

void function(int*);

and a top-level const pointer parameter:

// Same parameter type for overloading:
// void function(int* const);

Low-Level const Can Distinguish Overloads

These are different:

void print(std::string& text);
void print(const std::string& text);

Likewise:

void print(int* pointer);
void print(const int* pointer);

Selecting Const and Non-Const Overloads

void inspect(std::string& text)
{
std::cout << "modifiable\n";
}

void inspect(const std::string& text)
{
std::cout << "const\n";
}

Usage:

std::string a = "hello";
const std::string b = "world";

inspect(a);
inspect(b);

Output:

modifiable
const

For a non-const argument, the non-const overload is the better match.


Calling an Overloaded Function

Possible results of overload resolution are:

best match
no match
ambiguous call

Example:

void print(int);
void print(std::string);

Valid:

print(10);

No match:

// depending on available conversions:
// print(some_unrelated_type);

6.4.1 Overloading and Scope

Overloading occurs among functions found in the same scope.

Consider:

void print(int);
void print(double);

Inside another function:

void test()
{
void print(const std::string&);

print("hello");
}

The local declaration hides the outer print functions.

The compiler does not combine them into one overload set across scopes.


Name Lookup Happens First

Conceptually, C++ first determines:

What declarations named print are visible?

and only then asks:

Which visible print best matches the arguments?

An inner declaration can therefore hide otherwise valid outer overloads.

Normally, overload related functions should be declared together in the same scope.


6.5 Features for Specialized Uses

Important specialized function facilities include:

  • default arguments
  • inline
  • constexpr
  • debugging helpers

1) Default Arguments

Default Argument: A parameter value used when the caller does not supply that argument.

std::string make_window(
std::size_t height = 24,
std::size_t width = 80,
char background = ' ');

Calls:

make_window();

uses:

height = 24
width = 80
background = ' '

Call:

make_window(40);

uses:

height = 40
width = 80
background = ' '

Call:

make_window(40, 120);

uses:

height = 40
width = 120
background = ' '

Call:

make_window(40, 120, '#');

uses all explicitly supplied values.


Trailing Default Rule

Once a parameter has a default argument, all parameters to its right must also have defaults.

Valid:

void function(
int a,
int b = 10,
int c = 20);

Invalid:

// void function(
// int a = 10,
// int b,
// int c = 20);

Only Trailing Arguments Can Be Omitted

Given:

void function(
int a,
int b = 10,
int c = 20);

Valid:

function(1);
function(1, 2);
function(1, 2, 3);

You cannot skip b while explicitly supplying c.

Arguments match parameters by position.


Default Argument Declaration

Defaults are normally placed in the header declaration.

// display.hpp

void display(
int width = 80,
int height = 24);

Definition:

// display.cpp

#include "display.hpp"

void display(
int width,
int height)
{
// ...
}

Do not repeat the same default argument in the definition.


Default Argument Expressions

A default can be an expression.

int default_size()
{
return 100;
}

void create(
int size = default_size());

The expression is evaluated when the function is called without that argument.


2) inline and constexpr Functions

inline

Small functions are often defined as inline.

inline int square(int value)
{
return value * value;
}

Historically, inline requests that the compiler consider replacing a function call with the function body.

Conceptually:

int result = square(5);

might behave as though written:

int result = 5 * 5;

However, the compiler decides whether actual inline expansion is beneficial.

The important language property is also that an inline function can be defined identically in multiple translation units.


constexpr Function

A constexpr function can participate in compile-time evaluation.

constexpr int square(int value)
{
return value * value;
}

Compile-time use:

constexpr int result =
square(5);

static_assert(result == 25);

The same function may also be called using a runtime value.

int value = 0;

std::cin >> value;

int result = square(value);

Here the calculation occurs at runtime.


constexpr and Constant Expressions

constexpr int cube(int value)
{
return value * value * value;
}

constexpr int result = cube(3);

Result:

27

A constexpr function does not mean every call is evaluated at compile time.

It means the function can be evaluated at compile time when the call satisfies constant-expression requirements.


Header Definitions

Inline and constexpr functions are commonly defined directly in headers.

// math.hpp

#pragma once

inline int add(int a, int b)
{
return a + b;
}

constexpr int square(int value)
{
return value * value;
}

3) Aids for Debugging

assert

Required header:

#include <cassert>

assert checks a condition during execution.

int divide(int a, int b)
{
assert(b != 0);

return a / b;
}

If:

b != 0

is true, execution continues.

If false, the assertion reports a failure and terminates the program.


Appropriate Use of assert

Assertions are useful for conditions that should never be false if the program itself is correct.

void process(
const std::vector<int>& values)
{
assert(!values.empty());

// ...
}

They are not a replacement for handling normal user input errors.

For example, invalid user input should usually be handled normally rather than relying solely on an assertion.


NDEBUG

If NDEBUG is defined, ordinary assert checks are disabled.

#define NDEBUG

#include <cassert>

Build systems commonly define NDEBUG for release configurations.

Therefore, program correctness must not depend on code inside assert.

Wrong:

// Do not rely on the side effect:
assert(++value > 0);

When assertions are disabled, ++value would not occur.


Conditional Debug Code

#ifndef NDEBUG

std::cerr
<< "debug: value = "
<< value
<< '\n';

#endif

This code is compiled only when NDEBUG is not defined.


Predefined Debugging Names

Useful predefined identifiers include:

__func__
__FILE__
__LINE__
__DATE__
__TIME__

Example:

#ifndef NDEBUG

std::cerr
<< "file: "
<< __FILE__
<< '\n'
<< "function: "
<< __func__
<< '\n'
<< "line: "
<< __LINE__
<< '\n';

#endif

These are useful when tracing errors during development.


6.6 Function Matching

When an overloaded function is called, the compiler performs overload resolution.

The process can be understood in three stages:

candidate functions

viable functions

best match

Candidate Functions

Candidate Function: A visible function with the correct name.

void print(int);
void print(double);
void print(const std::string&);

For:

print(10);

all visible functions named print are initially candidates.


Viable Functions

Viable Function: A candidate that can accept the supplied arguments.

For:

print(10);

These might both be viable:

void print(int);
void print(double);

because 10 can be:

int

or converted to:

double

Best Match

The compiler prefers the function requiring the best conversions.

void print(int);
void print(double);

print(10);

The selected function is:

print(int);

because it is an exact match.


No Match

void function(int, int);

function(10);

There are too few arguments, so the function is not viable.

Compilation fails unless another overload can accept one argument.


Default Arguments and Viability

void function(
int a,
int b = 0);

Both are valid:

function(10);
function(10, 20);

The default makes the function viable for a one-argument call.


Multiple Parameters

A best function must be:

  • no worse than competing functions for every argument
  • better for at least one argument

Consider:

void function(int, double);
void function(double, int);

Call:

function(10, 10);

First overload:

first argument exact
second argument conversion

Second overload:

first argument conversion
second argument exact

Neither is better for all arguments.

The call is ambiguous.


1) Argument Type Conversions

Conversion quality is generally ranked as:

  1. exact match
  2. const qualification conversion
  3. promotion
  4. arithmetic or pointer conversion
  5. class-type conversion

Exact Match

void function(int);

int value = 10;

function(value);

No conversion is required.


Const Conversion

void inspect(const int& value);

int value = 10;

inspect(value);

The non-const object can bind to a reference to const.


Promotion

void function(int);

short value = 10;

function(value);

short is promoted to int.


Arithmetic Conversion

void function(double);

int value = 10;

function(value);

The int is converted to double.


Promotion Beats General Conversion

Consider:

void function(int);
void function(double);

short value = 10;

function(value);

The compiler prefers:

function(int);

because:

short -> int

is an integral promotion.

Whereas:

short -> double

is a general arithmetic conversion.


Arithmetic Conversions of Equal Rank

Consider:

void function(long);
void function(float);

double value = 3.14;

If both available conversions have the same rank and neither is better by the overload rules, a call may become ambiguous.

Do not assume that one arithmetic conversion is automatically preferred merely because the destination type seems "closer" conceptually.


Function Matching and const

void print(std::string&);
void print(const std::string&);

With:

std::string value = "hello";

the preferred overload is:

print(std::string&);

because it binds directly without adding const qualification.

With:

const std::string value = "hello";

only:

print(const std::string&);

is viable.


6.7 Pointers to Functions

Function Pointer: A pointer containing the address of a function.

Suppose we have:

bool length_compare(
const std::string& a,
const std::string& b)
{
return a.size() < b.size();
}

Its function type is determined by:

return type:
bool

parameter types:
const std::string&
const std::string&

Declaring a Function Pointer

bool (*compare)(
const std::string&,
const std::string&);

Read from the name outward:

compare

*compare

pointer

(...)

to function

bool

returning bool

The parentheses are essential.

Without them:

bool* compare(
const std::string&,
const std::string&);

this declares a function returning bool*, not a pointer to a function.


Assigning a Function

A function name automatically converts to a function pointer when needed.

compare = length_compare;

Using & is also valid:

compare = &length_compare;

Both mean the same thing here.


Calling Through a Function Pointer

bool result =
compare("hello", "world!");

A function pointer does not need to be explicitly dereferenced.

This is also valid:

bool result =
(*compare)("hello", "world!");

And of course:

bool result =
length_compare("hello", "world!");

All three call the same function.


Function Pointer Type Must Match

Suppose:

bool compare_strings(
const std::string&,
const std::string&);

int compare_ints(
int,
int);

A pointer declared as:

bool (*pointer)(
const std::string&,
const std::string&);

may point to:

pointer = compare_strings;

but not:

// Error:
// pointer = compare_ints;

The parameter and return types differ.


Null Function Pointer

A function pointer can represent no function.

bool (*compare)(
const std::string&,
const std::string&)
= nullptr;

Check before calling:

if (compare)
{
bool result =
compare("a", "bb");
}

Pointers to Overloaded Functions

Suppose:

void process(int*);
void process(unsigned int);

The function-pointer type selects the overload.

void (*pointer)(unsigned int) =
process;

This selects:

void process(unsigned int);

The pointer type must match one overload exactly.


Function Pointer Parameters

A function pointer can be passed to another function.

void use_compare(
const std::string& a,
const std::string& b,
bool (*compare)(
const std::string&,
const std::string&))
{
if (compare(a, b))
{
std::cout << a << '\n';
}
else
{
std::cout << b << '\n';
}
}

Call:

use_compare(
"hello",
"world!",
length_compare
);

The function name automatically converts to a function pointer.


Function-Type Parameter Syntax

This parameter:

void use_compare(
const std::string& a,
const std::string& b,
bool compare(
const std::string&,
const std::string&));

is automatically adjusted to a pointer-to-function parameter.

It is equivalent to:

void use_compare(
const std::string& a,
const std::string& b,
bool (*compare)(
const std::string&,
const std::string&));

Function Type Alias

Function pointer declarations quickly become difficult to read.

A type alias simplifies them.

using Compare =
bool(
const std::string&,
const std::string&);

Compare is a function type.

A pointer to it is:

Compare* pointer =
length_compare;

Pointer Alias

Alternatively:

using ComparePointer =
bool (*)(
const std::string&,
const std::string&);

Then:

ComparePointer pointer =
length_compare;

Function Pointer Parameter with Alias

using Compare =
bool(
const std::string&,
const std::string&);

void use_compare(
const std::string& a,
const std::string& b,
Compare* compare)
{
// ...
}

This is much easier to read than repeatedly writing the complete pointer type.


decltype and Function Types

Given:

bool length_compare(
const std::string&,
const std::string&);

then:

decltype(length_compare)

is the function type itself.

It is not automatically a pointer type.

Therefore:

decltype(length_compare)* pointer =
length_compare;

The explicit * is required.


Returning a Function Pointer

A function cannot directly return another function.

It can return a pointer to a function.

Using an alias:

using Operation =
int(int, int);

Some operations:

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

int subtract(int a, int b)
{
return a - b;
}

Function returning a function pointer:

Operation* select_operation(
char operation)
{
if (operation == '+')
{
return add;
}

return subtract;
}

Usage:

auto operation =
select_operation('+');

int result =
operation(10, 5);

Result:

15

Trailing Return Type for Function Pointers

Without an alias:

auto select_operation(char operation)
-> int (*)(int, int)
{
if (operation == '+')
{
return add;
}

return subtract;
}

The trailing return form is usually easier to read than embedding the function name inside a complicated pointer declaration.


decltype for a Function-Pointer Return

Given:

int add(int, int);

A selector may be declared:

decltype(add)* select_operation(
char operation);

Remember:

decltype(add)
= function type

decltype(add)*
= pointer to that function type

Essential Study Checklist

  1. A function consists of a return type, name, parameter list, and body.
  2. Arguments initialize function parameters.
  3. A function call transfers control to the called function.
  4. return transfers control back to the caller.
  5. A void function does not return a value.
  6. Local automatic objects normally exist only while their block executes.
  7. A local static object preserves its value between calls.
  8. Function declarations normally belong in headers.
  9. Function definitions normally belong in source files.
  10. Separate compilation allows individual source files to be compiled independently.
  11. Pass by value copies the argument.
  12. Modifying a value parameter does not modify the caller's object.
  13. A pointer parameter receives a copy of the pointer.
  14. Dereferencing a pointer parameter can modify the pointed-to object.
  15. A reference parameter acts as an alias for the argument.
  16. Use references when a function needs to modify caller-owned objects.
  17. Use const T& for read-only access to larger objects.
  18. Reference parameters can provide additional output values.
  19. Top-level const on a value parameter does not distinguish overloads.
  20. Low-level const on pointer and reference parameters does distinguish types.
  21. Array arguments normally convert to pointers to their first elements.
  22. An ordinary array parameter does not know the original array size.
  23. Begin/end pointers represent a half-open range [begin, end).
  24. Array size can also be passed explicitly.
  25. A reference-to-array parameter preserves the array dimension.
  26. Multidimensional array parameters require the inner dimensions.
  27. argc contains the number of command-line argument strings.
  28. argv contains pointers to those strings.
  29. User command-line arguments begin at argv[1].
  30. initializer_list handles a varying number of same-type arguments.
  31. initializer_list elements are const.
  32. Prefer modern C++ facilities over ellipsis parameters for ordinary code.
  33. A non-void function must return an appropriate value.
  34. Never return a pointer or reference to a local automatic object.
  35. A function returning a reference produces an lvalue.
  36. A recursive function needs a stopping condition.
  37. A function cannot return an array directly.
  38. A function can return a pointer or reference to an array.
  39. Type aliases and trailing return types simplify complex array return types.
  40. Overloaded functions have the same name but different parameter lists.
  41. Return type alone cannot distinguish overloaded functions.
  42. Parameter names do not affect function types.
  43. Type aliases do not create new types.
  44. Top-level const does not create a different value-parameter overload.
  45. Low-level const can create distinct pointer/reference overloads.
  46. Function overloading operates within one scope.
  47. Inner declarations can hide outer overloads.
  48. Default arguments can replace omitted trailing arguments.
  49. Once a parameter has a default, parameters to its right must also have defaults.
  50. Default arguments are normally declared in headers.
  51. inline functions are commonly small functions defined in headers.
  52. constexpr functions can participate in compile-time evaluation.
  53. A constexpr function may also execute at run time.
  54. assert checks assumptions during debugging.
  55. NDEBUG disables ordinary assert checks.
  56. Program correctness must not depend on side effects inside assert.
  57. Overload resolution first finds candidate functions.
  58. Candidate functions are filtered into viable functions.
  59. The compiler then selects the best viable function.
  60. Exact matches are preferred over conversions.
  61. Promotions are preferred over general arithmetic conversions.
  62. Multiple equally good overloads can make a call ambiguous.
  63. A function pointer stores the address of a function.
  64. A function pointer type includes the function's return and parameter types.
  65. Parentheses are required in pointer-to-function declarations.
  66. A function name automatically converts to a function pointer when needed.
  67. A function can be called directly through a function pointer.
  68. Function pointer types must match their target functions.
  69. Function pointers can be passed as function arguments.
  70. Function type aliases simplify complicated pointer declarations.
  71. decltype(function) gives the function type, not a pointer type.
  72. Add * to a function type when a function pointer is required.
  73. A function cannot return a function directly.
  74. A function can return a pointer to another function.
  75. Trailing return types simplify functions that return function pointers.