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:
- return type
- function name
- parameter list
- 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:
- arguments are evaluated
- parameters are initialized
- control transfers to the function
- the function body executes
returntransfers 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:
- end marker
- begin/end pointers
- explicit size
- 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
| Operation | Meaning |
|---|---|
initializer_list<T> list | Empty 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_listfor 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:
with:
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
inlineconstexpr- 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:
- exact match
- const qualification conversion
- promotion
- arithmetic or pointer conversion
- 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
- A function consists of a return type, name, parameter list, and body.
- Arguments initialize function parameters.
- A function call transfers control to the called function.
returntransfers control back to the caller.- A
voidfunction does not return a value. - Local automatic objects normally exist only while their block executes.
- A local
staticobject preserves its value between calls. - Function declarations normally belong in headers.
- Function definitions normally belong in source files.
- Separate compilation allows individual source files to be compiled independently.
- Pass by value copies the argument.
- Modifying a value parameter does not modify the caller's object.
- A pointer parameter receives a copy of the pointer.
- Dereferencing a pointer parameter can modify the pointed-to object.
- A reference parameter acts as an alias for the argument.
- Use references when a function needs to modify caller-owned objects.
- Use
const T&for read-only access to larger objects. - Reference parameters can provide additional output values.
- Top-level
conston a value parameter does not distinguish overloads. - Low-level
conston pointer and reference parameters does distinguish types. - Array arguments normally convert to pointers to their first elements.
- An ordinary array parameter does not know the original array size.
- Begin/end pointers represent a half-open range
[begin, end). - Array size can also be passed explicitly.
- A reference-to-array parameter preserves the array dimension.
- Multidimensional array parameters require the inner dimensions.
argccontains the number of command-line argument strings.argvcontains pointers to those strings.- User command-line arguments begin at
argv[1]. initializer_listhandles a varying number of same-type arguments.initializer_listelements are const.- Prefer modern C++ facilities over ellipsis parameters for ordinary code.
- A non-
voidfunction must return an appropriate value. - Never return a pointer or reference to a local automatic object.
- A function returning a reference produces an lvalue.
- A recursive function needs a stopping condition.
- A function cannot return an array directly.
- A function can return a pointer or reference to an array.
- Type aliases and trailing return types simplify complex array return types.
- Overloaded functions have the same name but different parameter lists.
- Return type alone cannot distinguish overloaded functions.
- Parameter names do not affect function types.
- Type aliases do not create new types.
- Top-level
constdoes not create a different value-parameter overload. - Low-level
constcan create distinct pointer/reference overloads. - Function overloading operates within one scope.
- Inner declarations can hide outer overloads.
- Default arguments can replace omitted trailing arguments.
- Once a parameter has a default, parameters to its right must also have defaults.
- Default arguments are normally declared in headers.
inlinefunctions are commonly small functions defined in headers.constexprfunctions can participate in compile-time evaluation.- A
constexprfunction may also execute at run time. assertchecks assumptions during debugging.NDEBUGdisables ordinaryassertchecks.- Program correctness must not depend on side effects inside
assert. - Overload resolution first finds candidate functions.
- Candidate functions are filtered into viable functions.
- The compiler then selects the best viable function.
- Exact matches are preferred over conversions.
- Promotions are preferred over general arithmetic conversions.
- Multiple equally good overloads can make a call ambiguous.
- A function pointer stores the address of a function.
- A function pointer type includes the function's return and parameter types.
- Parentheses are required in pointer-to-function declarations.
- A function name automatically converts to a function pointer when needed.
- A function can be called directly through a function pointer.
- Function pointer types must match their target functions.
- Function pointers can be passed as function arguments.
- Function type aliases simplify complicated pointer declarations.
decltype(function)gives the function type, not a pointer type.- Add
*to a function type when a function pointer is required. - A function cannot return a function directly.
- A function can return a pointer to another function.
- Trailing return types simplify functions that return function pointers.