CHAPTER 11

image

Functions

Functions are reusable code blocks that will only execute when called.

Defining Functions

A function can be created by typing void followed by the function’s name, a set of parentheses and a code block. The void keyword means that the function will not return a value. The naming convention for functions is the same as for variables – a descriptive name with each word initially capitalized, except for the first one.

void myFunction()
{
  cout << "Hello World";
}

Calling Functions

The function above will simply print out a text message when it is called. To invoke it from the main function the function’s name is specified followed by a set of parentheses.

int main()
{
  myFunction(); // "Hello World"
}

Function Parameters

The parentheses that follow the function name are used to pass arguments to the function. To do this the corresponding parameters must first be added to the function declaration in the form of a comma separated list.

void myFunction(string a, string b)
{
  cout << a + " " + b;
}

A function can be defined to take any number of parameters, and they can have any data types. Just ensure the function is called with the same types and number of arguments.

myFunction("Hello", "World"); // "Hello World"

To be precise, parameters appear in function definitions, while arguments appear in function calls. However, the two terms are sometimes used interchangeably.

Default Parameter Values

It is possible to specify default values for parameters by assigning them a value inside the parameter list.

void myFunction(string a, string b = "Earth")
{
  cout << a + " " + b;
}

Then, if that argument is unspecified when the function is called the default value will be used instead. For this to work it is important that the parameters with default values are to the right of those without default values.

myFunction("Hello"); // "Hello Earth"

Function Overloading

A function in C++ can be defined multiple times with different arguments. This is a powerful feature called function overloading that allows a function to handle a variety of parameters without the programmer using the function needing to be aware of it.

void myFunction(string a, string b) { cout << a+" "+b; }
void myFunction(string a)           { cout << a; }
void myFunction(int a)              { cout << a; }

Return Statement

A function can return a value. The void keyword is then replaced with the data type the function will return, and the return keyword is added to the function’s body followed by an argument of the specified return type.

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

Return is a jump statement that causes the function to exit and return the specified value to the place where the function was called. For example, the function above can be passed as an argument to the output stream since the function evaluates to an integer.

cout << getSum(5, 10); // 15

The return statement can also be used in void functions to exit before the end block is reached.

void dummy() { return; }

Note that although the main function is set to return an integer type, it does not have to explicitly return a value. This is because the compiler will automatically add a return zero statement to the end of the main function.

int main() { return 0; }

Forward Declaration

An important thing to keep in mind in C++ is that functions must be declared before they can be called. This does not mean that the function has to be implemented before it is called. It only means that the function’s header needs to be specified at the beginning of the source file, so that the compiler knows that the function exists. This kind of forward declaration is known as a prototype.

void myFunction(int a); // prototype int main()
{
  myFunction(0);
}
void myFunction(int a) {}

The parameter names in the prototype do not need to be included. Only the data types must be specified.

void myFunction(int);

Pass by Value

In C++, variables of both primitive and object data types are by default passed by value. This means that only a copy of the value or object is passed to the function. Therefore, changing the parameter in any way will not affect the original, and passing a large object will be very slow.

#include <iostream>
#include <string>
using namespace std;

void change(int i) { i = 10; }
void change(string s) { s = "Hello World"; }

int main()
{
  int x = 0;     // value type change(x);    // value is passed
  cout << x;     // 0

  string y = ""; // reference type
  change(y);     // object copy is passed
  cout << y;     // ""
}

Pass by Reference

Alternatively, to instead pass a variable by reference you just need to add an ampersand before the parameter’s name in the function’s definition. When arguments are passed by reference, both primitive and object data types can be changed or replaced and the changes will affect the original.

void change(int& i) { i = 10; }

int main()
{
  int x = 0; // value type
  change(x); // reference is passed
  cout << x; // 10
}

Pass by Address

As an alternative to passing by reference, arguments may also be passed by address using pointers. This passing technique serves the same purpose as passing by reference, but uses pointer syntax instead.

void change(int* i) { *i = 10; }

int main()
{
  int x = 0;  // value type
  change(&x); // address is passed
  cout << x;  // 10
}

Return by Value, Reference or Address

In addition to passing variables by value, reference or address, a variable may also be returned in one of these ways. Most commonly, a function returns by value, in which case a copy of the value is returned to the caller.

int byVal(int i) { return i + 1; }

int main()
{
  int a = 10;
  cout << byVal(a); // 11
}

To return by reference instead, an ampersand is placed after the function’s return type. The function must then return a variable and may not return an expression or literal, as can be done when using return by value. The variable returned should never be a local variable, since the memory to these variables is released when the function ends. Instead, return by reference is commonly used to return an argument that has also been passed to the function by reference.

int& byRef(int& i) { return i; }

int main()
{
  int a = 10;
  cout << byRef(a); // 10
}

To return by address the dereference operator is appended to the function’s return type. This return technique has the same two restrictions as when returning by reference – the address of a variable must be returned and that returned variable must not be local to the function.

int* byAdr(int* i) { return i; }

int main()
{
   int a = 10;
   cout << *byAdr(&a); // 10
}

Inline Functions

A thing to keep in mind when using functions is that every time a function is called, a performance overhead occurs. To potentially remove this overhead you can recommend that the compiler inlines the calls to a specific function by using the inline function modifier. This keyword is best suited for small functions that are called inside loops. It should not be used on larger functions since inlining these can severely increase the size of the code, which will instead decrease performance.

inline int myInc(int i) { return i++; }

Note that the inline keyword is only a recommendation. The compiler may in its attempts to optimize the code choose to ignore this recommendation and it may also inline functions that do not have the inline modifier.

Auto and Decltype

Two new keywords were introduced in C++11: auto and decltype. Both of these keywords are used for type deduction during compilation. The auto keyword works as a placeholder for a type and instructs the compiler to automatically deduce the type of the variable based on its initializer.

auto i = 5;     // int
auto d = 3.14;  // double
auto b = false; // bool

Auto translates to the core type of the initializer, which means that any reference and constant specifiers are dropped.

int& iRef = i;
auto myAuto = iRef; // int

Dropped specifiers can be manually reapplied as needed. The ampersand here creates a regular (lvalue) reference.

auto& myRef = iRef; // int&

Alternatively, two ampersands can be used. This normally designates an rvalue reference, but in the case of auto it makes the compiler automatically deduce either an rvalue or an lvalue reference, based on the given initializer.

int i = 1;
auto&& a = i; // int& (lvalue reference)
auto&& b = 2; // int&& (rvalue reference)

The auto specifier may be used anywhere a variable is declared and initialized. For instance, the type of the for loop iterator below is set to auto, since the compiler can easily deduce the type.

#include <vector>
using namespace std;
// ...
vector<int> myVector { 1, 2, 3 };
for (auto& x : myVector) { cout << x; } // "123"

Prior to C++11 there was no range-based for loop or auto specifier. Iterating over a vector then required a more verbose syntax.

for(vector<int>::size_type i = 0; i != myVector.size(); i++) {
    cout << myVector[i]; // "123"
}

The decltype specifier works similar to auto, except it deduces the exact declared type of a given expression, including references. This expression is specified in parentheses.

decltype(3) b = 3; // int&&

In C++14, auto may be used as the expression for decltype. The keyword auto is then replaced with the initializing expression, allowing the exact type of the initializer to be deduced.

decltype(auto) = 3; // int&&

Using auto is often the simpler choice when an initializer is available. Decltype is mainly used to forward function return types, without having to consider whether it is a reference or value type.

decltype(5) getFive() { return 5; } // int

C++11 added a trailing return type syntax, which allows a function’s return value to be specified after the parameter list, following the arrow operator (->). This enables the parameter to be used when deducing the return type with decltype. The use of auto in this context in C++11 just means that trailing return type syntax is being used.

auto getValue(int x) -> decltype(x) { return x; } // int

The ability to use auto for return type deduction was added in C++14. This enabled the core return type to be deduced directly from the return statement,

auto getValue(int x) { return x; } // int

Moreover, auto can be used together with decltype to deduce the exact type following the rules of decltype.

decltype(auto) getRef(int& x) { return x; } // int&

The main use for type deduction is to reduce the verbosity of the code and improve readability, particularly when declaring complicated types where the type is either difficult to know or difficult to write. Keep in mind that in modern IDEs you can hover over a variable to check its type, even if the type has been automatically deduced.

Lambda Functions

C++11 adds the ability to create lambda functions, which are unnamed function objects. This provides a compact way to define functions at their point of use, without having to create a named function somewhere else. The following example creates a lambda that accepts two int arguments and returns their sum.

auto sum = [](int x, int y) -> int
{
  return x + y;
};

cout << sum(2, 3); // "5"

Including the return type is optional if the compiler can deduce the return value from the lambda. In C++11 this required the lambda to contain just a single return statement, whereas C++14 extended return type deduction to any lambda function. Note that the arrow operator (->) is also omitted when leaving out the return type.

auto sum = [](int x, int y) { return x + y; };

C++11 requires lambda parameters to be declared with concrete types. This requirement was relaxed in C++14, allowing lambdas to use auto type deduction.

auto sum = [](auto x, auto y) { return x + y; };

Lambdas are typically used for specifying simple functions that are only referenced once, often by passing the function object as an argument to another function. This can be done using a function wrapper with a matching parameter list and return type, as in the following example.

#include <iostream>
#include <functional>
using namespace std;

void call(int arg, function<void(int)> func) {
  func(arg);
}

int main() {
 auto printSquare = [](int x) { cout << x*x; };
 call(2, printSquare); // "4"
}

All lambdas start with a set of square brackets, called the capture clause. This clause specifies variables from the surrounding scope that can be used within the lambda body. This effectively passes additional arguments to the lambda, without the need to specify these in the parameter list of the function wrapper. The previous example can therefore be rewritten in the following way.

void call(function<void()> func) { func(); }

int main() {
 int i = 2;
 auto printSquare = [i]() { cout << i*i; };
 call(printSquare); // "4"
}

The variable is here captured by value and so a copy is used within the lambda. Variables can also be captured by reference using the familiar ampersand prefix. Note that the lambda is here defined and called in the same statement.

int a = 1;
[&a](int x) { a += x; }(2);
cout << a; // "3"

It is possible to specify a default capture mode, to indicate how any unspecified variable used inside the lambda is to be captured. A [=] means the variables are captured by value and [&] captures them by reference. Variables captured by value are normally constant, but the mutable specifier can be used to allow such variables to be modified.

int a = 1, b = 1;
[&, b]() mutable { b++; a += b; }();
cout << a << b; // "31"

As of C++14, variables may also be initialized inside the capture clause. If there is no variable with the same name in the outer scope, the variable’s type will be deduced as if by auto.

int a = 1;
[&, b = 2]() { a += b; }();
cout << a; // "3"
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.165.247