Pass arguments

There are 3 primary methods of passing arguments to functions: pass by value, pass by reference, and pass by address.

pass by value

Advantages of passing by value:

Disadvantages of passing by value:

When to use pass by value:

When not to use pass by value:

In most cases, pass by value is the best way to pass arguments to functions – it is flexible and safe.

pass by reference

Advantages of passing by reference:

Disadvantages of passing by reference:

When to use pass by reference:

When not to use pass by reference:

pass by pointer

Advantages of passing by address:

Disadvantages of passing by address:

When to use pass by address:

When not to use pass by address:

As you can see, pass by address and pass by reference have almost identical advantages and disadvantages. Because pass by reference is generally safer than pass by address, pass by reference should be preferred in most cases.

Rule: Prefer pass by reference to pass by address whenever applicable.

There is only “pass by value”

References are typically implemented by the compiler as pointers. This means that behind the scenes, pass by reference is essentially just a pass by address (with access to the reference doing an implicit dereference).

Also, pass by address is actually just passing an address by value!

Therefore, we can conclude that C++ really passes everything by value! The value of pass by address/reference comes solely from the fact that we can dereference the passed address to change the argument, which we can not do with a normal value parameter!

inline function

One major downside of functions is that every time a function is called, there is a certain amount of performance overhead that occurs. This is because the CPU must store the address of the current instruction it is executing (so it knows where to return to later) along with other registers, all the function parameters must be created and assigned values, and the program has to branch to a new location.

C++ offers a way to combine the advantages of functions with the speed of code written in-place: inline functions. The inline keyword is used to request that the compiler treat your function as an inline function. When the compiler compiles your code, all inline functions are expanded in-place – that is, the function call is replaced with a copy of the contents of the function itself, which removes the function call overhead!

The downside is that because the inline function is expanded in-place for every function call, this can make your compiled code quite a bit larger, especially if the inline function is long and/or there are many calls to the inline function.

Note that the inline keyword is only a recommendation – the compiler is free to ignore your request to inline a function. This is likely to be the result if you try to inline a lengthy function!

Finally, modern compilers have gotten really good at inlining functions automatically – better than humans in most cases. Even if you don’t mark a function as inline, the compiler will inline functions that it believes will result in performance increases. Thus, in most cases, there isn’t a specific need to use the inline keyword. Let the compiler handle inlining functions for you.

preprocessor macro vs inline function

Preprocessor macros are just substitution patterns applied to your code. They can be used almost anywhere in your code because they are replaced with their expansions before any compilation starts.

Inline functions are actual functions whose body is directly injected into their call site. They can only be used where a function call is appropriate.

Now, as far as using macros vs. inline functions in a function-like context, be advised that:

Function overload

Function overloading is a feature of C++ that allows us to create multiple functions with the same name, so long as they have different parameters.

Function return types are not considered for uniqueness

Note that the function’s return type is NOT considered when overloading functions.

How function calls are matched with overloaded functions

Making a call to an overloaded function results in one of three possible outcomes:

  1. A match is found. The call is resolved to a particular overloaded function.
  2. No match is found. The arguments can not be matched to any overloaded function.
  3. An ambiguous match is found. The arguments matched more than one overloaded function.

When an overloaded function is called, C++ goes through the following process to determine which version of the function will be called:

1
2
3
4
void print(char *value);
void print(int value);
 
print(0); // exact match with print(int)

Although 0 could technically match print(char*) (as a null pointer), it exactly matches print(int). Thus print(int) is the best match available.

For example:

1
2
3
4
void print(char *value);
void print(int value);
 
print('a'); // promoted to match print(int)

In this case, because there is no print(char), the char ‘a’ is promoted to an integer, which then matches print(int).

For example:

1
2
3
4
5
struct Employee; // defined somewhere else
void print(float value);
void print(Employee value);
 
print('a'); // 'a' converted to match print(float)

In this case, because there is no print(char), and no print(int), the ‘a’ is converted to a float and matched with print(float).

Note that all standard conversions are considered equal. No standard conversion is considered better than any of the others.

For example, we might define a class X and a user-defined conversion to int.

1
2
3
4
5
6
7
class X; // with user-defined conversion to int
 
void print(float value);
void print(int value);
 
X value; // declare a variable named cValue of type class X
print(value); // value will be converted to an int and matched to print(int)

Although value is of type class X, because this particular class has a user-defined conversion to int, the function call print(value) will resolve to the Print(int) version of the function.

Ambiguous matches

Ambiguous match will be resulted because of:

For example:

1
2
3
4
5
6
void print(unsigned int value);
void print(float value);
 
print('a');
print(0);
print(3.14159);

In the case of print('a'), C++ can not find an exact match. It tries promoting ‘a’ to an int, but there is no print(int) either. Using a standard conversion, it can convert ‘a’ to both an unsigned int and a floating point value. Because all standard conversions are considered equal, this is an ambiguous match.

print(0) is similar. 0 is an int, and there is no print(int). It matches both calls via standard conversion.

print(3.14159) might be a little surprising, as most programmers would assume it matches print(float). But remember that all literal floating point values are doubles unless they have the ‘f’ suffix. 3.14159 is a double, and there is no print(double). Consequently, it matches both calls via standard conversion.

Ambiguous matches are considered a compile-time error. Consequently, an ambiguous match needs to be disambiguated before your program will compile. There are two ways to resolve ambiguous matches:

  1. Often, the best way is simply to define a new overloaded function that takes parameters of exactly the type you are trying to call the function with. Then C++ will be able to find an exact match for the function call.

  2. Alternatively, explicitly cast the ambiguous parameter(s) to the type of the function you want to call. For example, to have print(0) call the print(unsigned int), you would do this:

1
print(static_cast<unsigned int>(0)); // will call print(unsigned int)

Matching for functions with multiple arguments

If there are multiple arguments, C++ applies the matching rules to each argument in turn. The function chosen is the one for which each argument matches at least as well as all the other functions, with at least one argument matching better than all the other functions. In other words, the function chosen must provide a better match than all the other candidate functions for at least one parameter, and no worse for all of the other parameters. (每个参数至少和其他的函数匹配的一样好,并且至少有一个参数比其他的函数匹配的更好)

In the case that such a function is found, it is clearly and unambiguously the best choice. If no such function can be found, the call will be considered ambiguous (or a non-match).

Default parameters

C++ support default parameter in:

  1. forward declaration THEN definition: function declaration
  2. definition only: function definition

For example:

1
2
3
4
void printValue(int x = 10, int y = 20, int z = 30)
{
    std::count << "Values: " << x << " " << y << " " << z << '\n';
}

Note that it is impossible to supply a user-defined value for z without also supplying a value for x and y. This is because C++ does not support a function call syntax such as printValue(,,3). This has two major consequences:

void printValue(int x=10, int y);  // not allowed

Default parameter and function overloading

It is important to note that default parameters do NOT count towards the parameters that make the function unique. Consequently, following is not allowed:

1
2
void printValue(int x);
void printValue(int x, int y = 20);

If the caller were to call printValue(10), the compiler would not be able to disambiguate whether the user wanted printValue(int, 20) or printValue(int).

Function Pointers

A pointer to a function could be defined as:

void (*foo)(int);

Remember the right-left rule.

Calling a function using a funtion pointer

There are two ways to do this:

  1. Explicitly dereference: (*foo)()
  2. Implicitly dereference: foo()

NOTE: Default arguments are resolved at compile-time (that is, if you don’t supply an argument for a defaulted parameter, the compiler substitutes one in for you when the code is compiled). However, function pointers are resolved at run-time. Consequently, default parameters can NOT be resolved when making a function call with a function pointer. You’ll explicitly have to pass in values for any defaulted parameters in this case.

Providing default functions

It is possible to set a default callback function parameter in a function call. For example:

bool ascending(int, int);
void selectionSort(int *array, int size, bool (*comp)(int, int) = ascending);

Making function pointers prettier with typedef

typedef can be used to make pointers to function look more like regular variables:

typedef bool (*validateFcn)(int, int);

This defines a type called validateFcn that is a pointer to a function that takes two ints and returns a bool.

Now instead of doing this:

void selectionSort(int *array, int size, bool (*comp)(int, int));

You can do this:

1
void selectionSort(int *array, int size, validateFcn comp);

One final NOTE: C++ doesn’t allow the conversion of function pointer to void pointer (or vice-versa).

std::vector capacity and stack behavior

std::vector contains two separate attributes: size and __cap__acity.

In the context of a std::vector,

std::vector will reallocate its memory if needed, but it would prefer not to, because resizing an array is computationally expensive. Consider the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <vector>
 
int main()
{
std::vector<int> array;
array = { 0, 1, 2, 3, 4 }; // okay, array length = 5
std::cout << "size: " << array.size() << "  capacity: " << array.capacity() << '\n';
 
array = { 9, 8, 7 }; // okay, array length is now 3!
std::cout << "size: " << array.size() << "  capacity: " << array.capacity() << '\n';
 
return 0;
}

This produces the following:


size: 5 capacity: 5

size: 3 capacity: 5


Array subscripts and at() are based on size, not capacity. If you try to access 4th element in example above, it will fail.

Stack behavior with std::vector

We can make use of 3 functions provided by std::vector to simulate stack behavior:

Let’s see an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void printStack(const std::vector<int> &stack)
{
    for (const auto &element : stack)
        std::cout << element << ' ';
    std::cout << "(cap " << stack.capacity() << " size " << stack.size() << ")\n";
}
 
int main()
{
    std::vector<int> stack;
 
    printStack(stack);
 
    stack.push_back(5); // push_back() pushes an element on the stack
    printStack(stack);
 
    stack.push_back(3);
    printStack(stack);
 
    stack.push_back(2);
    printStack(stack);
 
    std::cout << "top: " << stack.back() << '\n'; // back() returns the last element
 
    stack.pop_back(); // pop_back() back pops an element off the stack
    printStack(stack);
 
    stack.pop_back();
    printStack(stack);
 
    stack.pop_back();
    printStack(stack);
 
    return 0;
}

This prints:


(cap 0 size 0)

5 (cap 1 size 1)

5 3 (cap 2 size 2)

5 3 2 (cap 3 size 3)

top: 2

5 3 (cap 3 size 2)

5 (cap 3 size 1)

(cap 3 size 0)


Unlike array subscripts or at(), the stack-based functions will resize the std::vector if necessary. In the example above, the vector gets resized 3 times (from a capacity of 0 to 1, 1 to 2, and 2 to 3).

Because resizing the vector is expensive, we can tell the vector to allocate a certain amount of capacity up front using the reserve() function.

Handling Errors

Assert

An assert statement is a preprocessor macro that evaluates a conditional expression.

Assert functionality lives in <cassert> header.

Rule: Favor asset statement liberally throughout your code where you want your code to quit if error occurs.

NDEBUG and other considerations

The assert function comes with a small performance cost that is incurred each time the assert condition is checked. Furthermore, asserts should (ideally) never be encountered in production code (because your code should already be thoroughly tested). Consequently, many developers prefer that asserts are only active in debug builds. C++ comes with a way to turn off asserts in production code: #define NDEBUG:

1
2
3
#define NDEBUG

// all assert() calls will now be ignored to the end of the file

Exception

C++ provides one more method for detecting and handling errors known as exception handling. The basic idea is that when an error occurs, the error is “thrown”. If the current function does not “catch” the error, the caller of the function has a chance to catch the error. If the caller does not catch the error, the caller’s caller has a chance to catch the error. The error progressively moves up the stack until it is either caught and handled, or until main() fails to handle the error. If nobody handles the error, the program typically terminates with an exception error.

Ellipsis

C++ provides a special specifier known as ellipsis(aka “…”), that allows us to define functions which take a variable number of parameters.

Function that uses ellipsis take the form:

return_type function_name(argument_list, ...)

Note that functions that use ellipsis must have at least one non-ellipsis parameter.