Categories
Computer Science / Information Technology Language: C++

Functions in C++

Please note that in this section, we shall be only looking at the extra features C++ provides than the ones which are provided by C. C++ supports all the features that C provides and provides extra features upon the ones C has. Please make sure that you have gone through the C notes on functions before going ahead with this section.

Functions

A function is a set of instructions that can perform a specific task. This aids in reducing the size of the program by calling and using the functions any number of times from different places in the program instead of rewriting the same set of instructions everywhere. The advantages of functions are reusability, modularity and increased simplicity.

Types of functions

Functions are broadly classified into 2 types:

  • Library functions: These functions come with the C++ library and are ready to be used in the programs without worrying about internal implementation. Eg: iostream, string etc
  • User-defined functions: A function defined by the user.

Library functions

C++ has many built-in libraries. One such library is the cmath library (similar to math.h in C programming). Inorder to use this library, one must include cmath. This can be done using the following statement:

#include <cmath>

The cmath library includes functions to perform mathematical operations. A detailed list of all the functions can be found in the following link: https://cplusplus.com/reference/cmath/
Some of the most commonly used functions are listed below along with information on how to use them:

Sl. NoFunctionParametersReturn ValueDescription
1pow()Base, exponentThe result of raising base to the power exponent.Returns base raised to the power exponent
2sqrt()+ve value whose square root is computed.Square root of the parameterReturns the square root of x.
3ceil()Value to round upThe smallest integral value that is not less than x (as a floating-point value).Rounds x upward, returning the smallest integral value that is not less than x.
4floor()Value to round down.The value of x rounded downward (as a floating-point value).Rounds x downward, returning the largest integral value that is not greater than x.
5round()Value to round.The value of x rounded to the nearest integral (as a floating-point value).Returns the integral value that is nearest to x, with halfway cases rounded away from zero.
6fabs()Value whose absolute value is returned.The absolute value of x. Always returns a floating point.Returns the absolute value of x: |x|.
7abs()Value whose absolute value is returned.The absolute value of x. Floating point or integer depending on the parameter.Returns the absolute value of x: |x|.

Example

#include <iostream>
#include <cmath>

int main ()
{
    std::cout << "abs (3.1416) = " << std::abs (3.1416) << std::endl;
    std::cout << "abs (-10.6) = "   << std::abs (-10.6) << std::endl;
    std::cout << "7 ^ 3 = "        << pow (7.0, 3.0) << std::endl;
    std::cout << "4.73 ^ 12 = "    << pow (4.73, 12.0) << std::endl;
    std::cout << "32.01 ^ 1.54 = " << pow (32.01, 1.54) << std::endl;

    double param, result;
    param = 1024.0;
    result = sqrt (param);
    std::cout << "sqrt(" << param << ") = " << result << std::endl;

    std::cout << "value\tround\tfloor\tceil\tfabs\tabs" << std::endl;
    std::cout << "-----\t-----\t-----\t----\t----\t---" << std::endl;
    std::cout <<  2.3 << "\t" << round( 2.3) << "\t" << floor( 2.3) << "\t" << ceil( 2.3) << "\t" << fabs( 2.3) << "\t" <<  abs( 2) << std::endl;
    std::cout <<  3.8 << "\t" << round( 3.8) << "\t" << floor( 3.8) << "\t" << ceil( 3.8) << "\t" << fabs( 3.8) << "\t" <<  abs( 3.8) << std::endl;
    std::cout <<  5.5 << "\t" << round( 5.5) << "\t" << floor( 5.5) << "\t" << ceil( 5.5) << "\t" << fabs( 5.5) << "\t" <<  abs( 5.5) << std::endl;
    std::cout << -2.3 << "\t" << round(-2.3) << "\t" << floor(-2.3) << "\t" << ceil(-2.3) << "\t" << fabs(-2.3) << "\t" <<  abs( -2.3) << std::endl;
    std::cout << -3.8 << "\t" << round(-3.8) << "\t" << floor(-3.8) << "\t" << ceil(-3.8) << "\t" << fabs(-3.8) << "\t" <<  abs( -3.8) << std::endl;
    std::cout << -5.5 << "\t" << round(-5.5) << "\t" << floor(-5.5) << "\t" << ceil(-5.5) << "\t" << fabs(-5.5) << "\t" <<  abs( -5.5) << std::endl;

    return 0;
}

Output:

abs (3.1416) = 3.1416
abs (-10.6) = 10.6
7 ^ 3 = 343
4.73 ^ 12 = 1.2541e+08
32.01 ^ 1.54 = 208.037
sqrt(1024) = 32
value   round   floor   ceil    fabs    abs
-----   -----   -----   ----    ----    ---
2.3     2       2       3       2.3     2
3.8     4       3       4       3.8     3
5.5     6       5       6       5.5     5
-2.3    -2      -3      -2      2.3     2
-3.8    -4      -4      -3      3.8     3
-5.5    -6      -6      -5      5.5     5

The cmath library given above is an example of built in libraries and the utilities that comes with them. The programmer can easily import them and exploit the functions they provide.

User defined functions

As indicated earlier, one must continue reading this section only if he/she has gone through the functions in C notes completely.
C++ provides much more flexibility to the programmer to define her own functions that come along with the features provided by C. We will see only the features provided by C++ over that came with C.

Default argument values

As we know, when a function is called, all arguments must be supplied. However, sometimes some arguments have the same values most of the time. C++ provides a feature called default argument values that tells the compiler to use default values if the arguments are not supplied.

Default values can be in the prototype or in the definition, not in both. However, it is recommended as a best practice to have it in the prototype. It is also mandatory for the default argument values to appear at the tail end of the parameter list.

There could be multiple default values, all of which must appear consecutively at the tail end of the parameter list.
The following is an example that show a program with no default arguments:

#include <iostream>

// Function prototype
double calc_profit(double revenue, double investment);

// Function definition
double calc_profit(double revenue, double investment) {
    return revenue - investment;
}

int main () {
    double revenue = 5000;
    double investment = 2000;
    std::cout << "Profit: " << calc_profit(revenue, investment);
    return 0;
}

The same program could be written in the following way assuming that the investment will be always a same value of 2000:

#include <iostream>

// Function prototype
double calc_profit(double revenue, double investment = 2000);

// Function definition
double calc_profit(double revenue, double investment) {
    return revenue - investment;
}
int main () {
    double revenue = 5000;
    std::cout << "Profit: " << calc_profit(revenue);
    return 0;
}

Note that the function calc_profit() can still accept investment value. When explicitly sent, the default argument of 2000 will be overridden with another value sent. This can be seen in the following example:

#include <iostream>

// Function prototype
double calc_profit(double revenue, double investment = 2000);

// Function definition
double calc_profit(double revenue, double investment) {
    return revenue - investment;
}

int main () {
    double revenue = 5000;
    double investment = 200;
    std::cout << "Profit: " << calc_profit(revenue, investment);
    return 0;
}

In the examples so far, we have seen only one default parameter. However, we can have multiple default arguments for a function. The only condition is that the default argument list needs to be put at the tail end. Consider the following example:

#include <iostream>

// Function prototype
double calc_profit(double revenue, double investment = 2000, double tax = 18);

// Function definition
double calc_profit(double revenue, double investment, double tax) {
    double profit = revenue - investment;
    return profit - profit * tax / 100;
}

int main () {
    double revenue = 5000;
    std::cout << "Profit: " << calc_profit(revenue) << std::endl;
    std::cout << "Profit: " << calc_profit(revenue, 200) << std::endl;
    std::cout << "Profit: " << calc_profit(revenue, 200, 20);
    return 0;
}

Output:

Profit: 2460
Profit: 3936
Profit: 3840

Function overloading

Function overloading is a feature given by C++ that enables programmers to have the same function name for 2 or more functions of the same program but with a different signature, that is, different parameter list in terms of order of the data types, return type or the number of the parameters in the function.

This feature enables programmers to write readable, intuitive code that is easy to maintain. This feature comes as a type of polymorphism: same name with different data types to execute similar behaviour. The compiler must be able to tell the functions apart based on the parameter lists and arguments supplied.

Consider the following simple example of adding two numbers. As C++ is a strictly typed language (meaning, the data type of a variable once declared cannot be changed over the course of execution), and there can be multiple permutations and combinations of addition of numbers such as:

  • Float + Float
  • Int + Int
  • Float + Int
  • Int + Float

Naming differently for functions defining logic for the above combinations is firstly cumbersome, and also abstraction is badly hit. Using the feature of function overloading, a single name can be used to define all the four functions and this is demonstrated in the following example:

#include <iostream>

using namespace std;

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

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

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

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

int main() {
    int a = 10, b = 11;
    float c = 10.5, d = 11.8;
    cout << add(c, d) << endl;
    cout << add(a, b) << endl;
    cout << add(c, b) << endl;
    cout << add(a, d) << endl;
    return 0;
}

Notice that the name of the function is the same but each of the functions are unique with respect to their return type and/or the parameter list. Having the exact same name, parameter list and order of the data types in the parameter list will give rise to a compile time error as there is no way for the compiler to tell the functions apart when called. Consider the following function declarations.

void display(float n);
void display(char ch);
void display(std::string s);
void display(std::vector<std::string> v);

Once these functions are implemented, one can call display and pass in the data. The compiler will check the data type of the argument and match it to one of the available overloaded display functions. If it can’t match it or if it can’t convert the argument to one that matches, then we get a compiler error.

There is one restriction to function overloading. The return type is not considered when the compiler is trying to determine which function to call. Consider the following example where there are two overloaded functions with no parameters but with different return type:

int fetch_value();
float fetch_value();

// Compile time error
cout << fetch_value();

Pass by reference

At times, we want to be able to change the actual parameter from within the function body. In order to achieve this we need to have access to the location/address of the actual parameter inside of the called function. There are two ways to achieve this: using pointers and using references. In this section, we will see how to do this by passing values by reference. The syntax to call by reference is as follows:

ret_type called_function (data_type &formal_parameter) {
    // Body of the function
}

ret_type caller_function () {
    called_function(actual_parameter)
}

Consider the following example:

#include <iostream>
using std::cout;

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

int main() {
    int num {10};
    int sq_num = square(num);
    cout << num << " * " << num << " = " << sq_num;
    return 0;
}

In the above example, the formal parameter is a copy of the actual parameter. The function square() squares the variable num and returns it. The returned value is copied to the variable sq_num and then printed.

We might want the function to modify the actual parameter instead of making a copy of it and then operating on the copy. Let’s see how to do it using references in C++:

#include <iostream>
using std::cout;

void square(int &num) {
    num = num * num;
}

int main() {
    int num {10};
    int original_num {num};
    square(num);
    cout << original_num << " * " << original_num << " = " << num;
    return 0;
}

Notice that the formal parameter is an alias of the actual parameter and both of them refer to the same object in the memory. Any operation on the formal parameter inside of the function will reflect in the caller function too.

Pass by reference is advantageous for various reasons as follows:
It allows us to change the actual parameter if we need to.
We do not make a copy of the parameter, which could be large and take time. Pass by reference is far more efficient than passing by value.
Let’s see an example where we can swap two numbers.

#include <iostream>
using std::cout;

void swap(int &a, int &b) {
    a += b;
    b = a - b;
    a -= b;
}

int main() {
    int a {10}, b {11};
    cout << "Before swapping: a: " << a << " b: " << b << "\n";
    swap(a, b);
    cout << "After swapping: a: " << a << " b: " << b << "\n";
    return 0;
}

However, we need to be aware of potentially unwanted changes. Consider the following example:

#include <iostream>
#include <vector>

using std::cout;
using std::vector;
using std::endl;

void print(vector <int> &v) {
    for (auto num: v)
        cout << num << " ";
    cout << endl;
}

int main() {
    vector <int> vec {1,2,3,4,5};
    print(vec);
    return 0;
}

In the above example, the function print() has absolute control over the vector and has all privileges to modify it. One needs to follow the least privileges principle: if the permission is not needed to perform an operation, the permission need not be given. The function print() need not have write permission. Hence it can receive the vector as a const vector to prevent accidental modification. This is demonstrated in the following example:

#include <iostream>
#include <vector>

using std::cout;
using std::vector;
using std::endl;

void print(const vector <int> &v) {
    v[1] = 5;         // Error
    for (auto num: v)
        cout << num << " ";
    cout << endl;
}

int main() {
    vector <int> vec {1,2,3,4,5};
    print(vec);
    return 0;
}

C++ Function scope rules

C++ uses scope rules to determine where an identifier can be used. C++ uses static or lexical scoping. C++ has 2 main scopes:

Local or block scope
  1. Identifiers are declared in a block: {}, hence only visible within the block {} where declared.
#include <iostream>

void function() {
    int num = 100;
}

int main() {
    std::cout<<num;     // Error as num is not in scope of main
    return 0;
}
  1. Function parameters have block scope, hence they cannot be accessed outside of the function as the block is the function itself. (Refer C notes on functions)
#include <iostream>

void function(int num) {
    std::cout << num;
}

int main() {
    int n = 100;
    std::cout<<num; // num is local only to function()
    return 0;
}
  1. Function local variables are only active and available while the function is executing.
  2. Local variables are not preserved between function calls.
  3. With nested blocks inner blocks can access outer variables but outer blocks cannot access inner variables.
#include <iostream>
using std::cout;

void function() {
    int num = 100;
    {
        int num = 200;
        cout << num << "\n";
    }
    cout << num;
}

int main() {
    function();
    return 0;
}

Output:

200
100
#include <iostream>
using std::cout;

void function() {
    int num = 100;
    {
        int n = 200;
        cout << num << "\n";
        cout << n << "\n";
    }
    cout << n;  // Error
}

int main() {
    function();
    return 0;
}
  1. The only type of variable whose value is preserved between function calls is a static qualifier. However, these are only initialised the first time the function is called. The life of static variables is throughout the execution of the program. But these variables can be accessed only inside of their respective blocks.
#include <iostream>
using std::cout;

void function() {
    static int n {100};
    cout << n << "\n";
    n++;
}

int main() {
    function();
    function();
    function();
    return 0;
}

Output:

100
101
102
Global Scope
  1. These identifiers are declared outside any function or class.
  2. Identifiers with global scope are visible to all parts of the program after they have been declared.
  3. The best practice is not to use global variables. However, declaring constants such as PI, size of a fixed buffer are okay to be done in global scope.
#include <iostream>
using std::cout;
const int global_const {100};

void function() {
    cout << global_const << "\n";
}

int main() {
    function();
    cout << global_const << "\n";
    return 0;
}

Output:

100
100

Inline functions

As we have seen in C notes, function calls have significant overhead associated with them. The activation record has to be created, push it on the stack, deal with parameters and execute the logic, pop off the activation record when the function terminates and deal with the return addresses and return values. Although this can happen very quickly and very efficiently, this happens. Sometimes, we have a very simple function where the function call overhead might be greater than the function execution time itself.

In such cases, we can suggest the compiler to generate inline code and embed the function logic into the caller function itself. Inline code is basically inline assembly code that avoids the function overhead. Inline functions are generally faster but this needs to be used judiciously. If these are used frequently for the same piece of logic, you are violating the DRY principle of coding: Do not repeat yourself. Repetition results in larger binaries. However, compiler technology has grown significantly that they are now capable of detecting which are the functions which can be made inline for maximum efficiency.

The following is the syntax for the inline functions:

inline <return type> <function name> ([parameter list]) {
    // function definition
}

The following is an example of inline function

#include <iostream>
using std::cout;

inline int subtract(int op1, int op2) {
    return op1 - op2;
}

int main() {
    cout << subtract(4, 3);
    return 0;
}

Output:

1

Leave a Reply

Your email address will not be published. Required fields are marked *

You cannot copy content of this page