HOME

Weikai Mao

Weikai Mao

maoweikai123@outlook.com

Categories:

© 2024

Notes on "C++ Primer": Chapter 6. Functions


  • 6.5 Features for Specialized Uses
  • 6.6 Function Matching
  • 6.6.1 Argument Type Conversions
  • 6.7 Pointers to Functions
  • Chapter Summary
  • Defined Terms
  • References

  • Chapter 6. Functions

    This chapter describes how to define and declare functions.

    A function is a block of code with a name. We execute the code by calling the function.

    6.1 Function Basics

    A function definition typically consists of a return type, a name, a list of zero or more parameters, and a body.

    We execute a function through the call operator, which is a pair of parentheses. Inside the parentheses is a comma-separated list of arguments. The arguments are used to initialize the function’s parameters.

    Writing a Function
    // factorial
    int fact(int val) {
        int ret = 1; // local variable to hold the result as we calculate it
        while (val > 1)
            ret *= val--; // assign ret * val to ret and decrement val
        return ret; // return the result
    }
    
    float plus(float x, float y) {
        return x + y
    }
    
    Calling a Function

    A function call does two things: It initializes the function’s parameters from the corresponding arguments, and it transfers control to that function, then execution of the called function begins.

    A call to fact:

    int j = fact(5);
    

    is equivalent to

    int val = 5; // initialize val from the literal 5
    int ret = 1; // code from the body of fact
    while (val > 1)
        ret *= val--;
    int j = ret; // initialize j as a copy of ret
    
    Parameters and Arguments

    Arguments are the initializers for a function’s parameters. We have no guarantees about the order in which arguments are evaluated (§ 4.1.3, p. 137). The compiler is free to evaluate the arguments in whatever order it prefers.

    fact(3.14); // ok: argument is converted to int; this is equivalent to fact(3);
    
    Function Parameter List

    A function’s parameter list can be empty but cannot be omitted.

    void f1() { /* ... */ } // implicit void parameter list
    void f2(void) { /* ... */ } // explicit void parameter list
    
    Function Return Type

    Most types can be used as the return type of a function. In particular, the return type can be void, which means that the function does not return a value. However, the return type may not be an array type (§ 3.5, p. 113) or a function type. However, a function may return a pointer to an array or a function.

    6.1.1 Local Objects

    In C++, names have scope (§ 2.2.4, p. 48), and objects have lifetimes. It is important to understand both of these concepts.

    • The scope of a name is the part of the program’s text in which that name is visible.

    • The lifetime of an object is the time during the program’s execution that the object exists.

    The body of a function is a statement block that forms a new scope in which we can define variables. Parameters and variables defined inside a function body are referred to as local variables. They are “local” to that function and hide declarations of the same name made in an outer scope.

    Automatic Objects

    Objects that exist only while a block is executing are known as automatic objects. After execution exits a block, the values of the automatic objects created in that block are undefined.

    Parameters are automatic objects. Storage for the parameters is allocated when the function begins. Parameters are defined in the scope of the function body. Hence they are destroyed when the function terminates.

    Automatic objects corresponding to local variables are initialized if their definition contains an initializer.

    Local static Objects

    It can be useful to have a local variable whose lifetime continues across calls to the function. We obtain such objects by defining a local variable as static. Each local static object is initialized before the first time execution passes through the object’s definition. Local statics are not destroyed when a function ends; they are destroyed when the program terminates.

    As a trivial example, here is a function that counts how many times it is called:

    size_t count_calls()
    {
        static size_t ctr = 0; // value will persist across calls
        return ++ctr;
    }
    
    int main()
    {
        for (size_t i = 0; i != 10; ++i)
            cout << count_calls() << endl;
        return 0;
    }
    

    This program will print the numbers from 1 through 10 inclusive.

    Before control flows through the definition of ctr for the first time, ctr is created and given an initial value of 0. Whenever count_calls is executed, the variable ctr already exists and has whatever value was in that variable the last time the function exited.

    6.1.2 Function Declarations

    A function declaration (a.k.a. function prototype) is just like a function definition except that a declaration has no function body.

    void print(vector<int>::const_iterator beg,
               vector<int>::const_iterator end);
    

    Because a function declaration has no body, there is no need for parameter names, but they can help understanding the function.

    Function Declarations Go in Header Files

    As with variables, functions should also be declared in header files and defined in source files.

    The source file that defines a function should include the header that contains that function’s declaration. That way the compiler will verify that the definition and declaration are consistent.

    6.1.3 Separate Compilation

    We may want to store the various parts of the program in separate files. C++ supports what is commonly known as separate compilation to lets us split our programs into several files, each of which can be compiled independently.

    Compiling and Linking Multiple Source Files

    As an example, assume that the definition of our fact function is in a file named fact.cc and its declaration is in a header file named Chapter6.h. Our fact.cc file, like any file that uses these functions, will include the Chapter6.h header. We’ll store a main function that calls fact in a second file named factMain.cc.

    To produce an executable file, we must tell the compiler where to find all of the code we use. We might compile these files as follows:

    $ CC factMain.cc fact.cc # generates factMain.exe or a.out
    $ CC factMain.cc fact.cc -o main # generates main or main.exe
    

    Here CC is the name of our compiler, $ is our system prompt, and # begins a command-line comment.

    If we have changed only one of our source files, we’d like to recompile only the file that actually changed. Most compilers provide a way to separately compile each file. This process usually yields a file with the .obj (Windows) or .o (UNIX) file extension, indicating that the file contains object code.

    $ CC -c factMain.cc # generates factMain.o
    $ CC -c fact.cc # generates fact.o
    $ CC factMain.o fact.o # generates factMain.exe or a.out
    $ CC factMain.o fact.o -o main # generates main or main.exe
    

    6.2 Argument Passing

    As we’ve seen, each time we call a function, its parameters are created and initialized by the arguments passed in the call.

    Note: Parameter initialization works the same way as variable initialization.

    As with any other variable, the type of a parameter determines the interaction between the parameter and its argument. If the parameter is a reference (§ 2.3.1, p. 50), then the parameter is bound to its argument. Otherwise, the argument’s value is copied. When a parameter is a reference, we say that its corresponding argument is “passed by reference” or that the function is “called by reference”.

    When the argument value is copied, the parameter and argument are independent objects. We say such arguments are “passed by value” or alternatively that the function is “called by value”.

    6.2.1 Passing Arguments by Value

    When we initialize a nonreference type variable, the value of the initializer is copied. Changes made to the variable have no effect on the initializer.

    Passing an argument by value works exactly the same way.

    Pointer Parameters

    Pointers (§ 2.3.2, p. 52) behave like any other nonreference type.

    // function that takes a pointer and sets the pointed-to value to zero
    void reset(int *ip)
    {
        *ip = 0; // changes the value of the object to which ip points
        ip = 0; // changes only the local copy of ip; the argument is unchanged
    }
    

    Best Practice: Programmers accustomed to programming in C often use pointer parameters to access objects outside a function. In C++, programmers generally use reference parameters instead.

    6.2.2 Passing Arguments by Reference

    Reference parameters are often used to allow a function to change the value of its arguments.

    // function that sets the given object to 0
    void reset(int &i)
    {
        i = 0;
    }
    

    As with any other reference, a reference parameter is bound directly to the object from which it is initialized. We pass an object directly; there is no need to pass its address:

    int j = 42;
    reset(j); // j is passed by reference; the value in j is changed
    
    Using References to Avoid Copies

    It can be inefficient to copy objects of large class types or large containers. Moreover, some class types (including the IO types) cannot be copied.

    // compare the length of two strings
    bool isShorter(const string &s1, const string &s2)
    {
        return s1.size() < s2.size();
    }
    

    Best Practice: Reference parameters that are not changed inside a function should be references to const.

    Using Reference Parameters to Return Additional Information

    A function can return only a single value. Reference parameters let us effectively return multiple results.

    6.2.3 const Parameters and Arguments

    Just as in any other initialization, when we copy an argument to initialize a parameter, top-level consts (§ 2.4.3, p. 63) on parameters are ignored. We can pass either a const or a nonconst object to a parameter (i in the following code) that has a top-level const:

    void fcn(const int i) { /* fcn can read but not write to i */ }
    

    In C++, we can define several different functions that have the same name. However, we can do so only if their parameter lists are sufficiently different, otherwise it’s an error. For example,

    void fcn(const int i) { /* fcn can read but not write to i */ }
    void fcn(int i) { /* ... */ } // error: redefines fcn(int)
    

    Because top-level consts are ignored, we can pass exactly the same types to either version of fcn. The second version of fcn is an error. Despite appearances, its parameter list doesn’t differ from the list in the first version of fcn.

    Pointer or Reference Parameters and const

    Remember that, we can initialize an object with a low-level const from a nonconst object but not vice versa (§ 2.4.1, p. 61; § 2.4.2, p. 62), and a plain reference must be initialized from an object of the same type (§ 2.3.1, p. 50).

    Exactly the same initialization rules apply to parameter passing:

    int i = 0;
    const int ci = i;
    string::size_type ctr = 0;
    reset(&i); // calls the version of reset that has an int* parameter
    reset(&ci); // error: can't initialize an int* from a pointer to a const int object
    reset(i); // calls the version of reset that has an int& parameter
    reset(ci); // error: can't bind a plain reference to the const object ci
    reset(42); // error: can't bind a plain reference to a literal
    reset(ctr); // error: types don't match; ctr has an unsigned type
    // ok: find_char (§ 6.2.2, p. 211) 's first parameter is a reference to const
    find_char("Hello World!", 'o', ctr);
    
    Use Reference to const When Possible

    As we’ve just seen, we cannot pass a const object, or a literal, or an object that requires conversion to a plain reference parameter.

    6.2.4 Array Parameters

    Arrays have two special properties that affect how we define and use functions that operate on arrays: We cannot copy an array (§ 3.5.1, p. 114), and when we use an array it is (usually) converted to a pointer (§ 3.5.3, p. 117). When we pass an array to a function, we are actually passing a pointer to the array’s first element, and the size of the array is irrelevant.

    Regardless of appearances, these declarations are equivalent: Each declares a function with a single parameter of type const int*.

    void print(const int*);
    void print(const int[]); // shows the intent that the function takes an array
    void print(const int[10]); // dimension for documentation purposes (at best)
    

    When the compiler checks a call to print, it checks only that the argument has type const int*:

    int i = 0, j[2] = {0, 1};
    print(&i); // ok: &i is int*
    print(j); // ok: j is converted to an int* that points to j[0]
    

    WARNING: As with any code that uses arrays, functions that take array parameters must ensure that all uses of the array stay within the array bounds.

    Because arrays are passed as pointers, functions ordinarily don’t know the size of the array they are given. They must rely on additional information provided by the caller. There are three common techniques used to manage pointer parameters.

    Using a Marker to Specify the Extent of an Array

    The first approach to manage array arguments requires the array itself to contain an end marker. For example, C-style strings (§ 3.5.4, p. 122) are stored in character arrays in which the last character of the string is followed by a null character.

    void print(const char *cp)
    {
        if (cp) // if cp is not a null pointer
            while (*cp) // so long as the character it points to is not a null character
                cout << *cp++; // print the character and advance the pointer
    }
    

    This convention works less well with data, such as ints, where every value in the range is a legitimate value.

    Using the Standard Library Conventions

    A second technique used to manage array arguments is to pass pointers to the first and one past the last element in the array.

    void print(const int *beg, const int *end)
    {
        // print every element starting at beg up to but not including end
        while (beg != end)
            cout << *beg++ << endl; // print the current element and advance the pointer
    }
    

    To call this function,

    int j[2] = {0, 1};
    print(begin(j), end(j)); // begin and end functions, see § 3.5.3 (p. 118)
    
    Explicitly Passing a Size Parameter

    A third approach for array arguments, which is common in C programs and older C++ programs, is to define a second parameter that indicates the size of the array.

    // const int ia[] is equivalent to const int* ia
    // size is passed explicitly and used to control access to elements of ia
    void print(const int ia[], size_t size)
    {
        for (size_t i = 0; i != size; ++i) {
            cout << ia[i] << endl;
        }
    }
    

    To call this function,

    int j[] = {0, 1};
    print(j, end(j) - begin(j));
    
    Array Parameters and const
    Array Reference Parameters

    We can define a parameter that is a reference to an array.

    // ok: parameter is a reference to an array; the dimension is part of the type
    void print(int (&arr)[10])
    {
        for (auto elem : arr)
            cout << elem << endl;
    }
    

    Note: The parentheses around &arr are necessary (§ 3.5.1, p. 114):

    f(int &arr[10]) // error: declares arr as an array of references
    f(int (&arr)[10]) // ok: arr is a reference to an array of ten ints
    

    The fact that the size is part of the type limits the usefulness of this version of print. We may call this function only for an array of exactly ten ints:

    int i = 0, j[2] = {0, 1};
    int k[10] = {0,1,2,3,4,5,6,7,8,9};
    print(&i); // error: argument is not an array of ten ints
    print(j); // error: argument is not an array of ten ints
    print(k); // ok: argument is an array of ten ints
    

    We’ll see in § 16.1.1 (p. 654) how we might write this function in a way that would allow us to pass a reference parameter to an array of any size.

    Passing a Multidimensional Array

    A multidimensional array is passed as a pointer to its first sub-array.

    // matrix points to the first element in an array whose elements are arrays of ten ints
    void print(int (*matrix)[10], int rowSize) { /* ... */ }
    

    declares matrix as a pointer to an array of ten ints.

    Note: Again, the parentheses around *matrix are necessary:

    int *matrix[10]; // array of ten pointers
    int (*matrix)[10]; // pointer to an array of ten ints
    

    We can also define our function using array syntax. As usual, the compiler ignores the first dimension, so it is best not to include it:

    // equivalent definition
    void print(int matrix[][10], int rowSize) { /* ... */ }
    

    declares matrix to be what looks like a two-dimensional array. In fact, it’s still a pointer to an array of ten ints.

    6.2.5 main: Handling Command-Line Options

    We sometimes need to pass arguments to main. The most common use of arguments to main is to let the user specify a set of options to guide the operation of the program.

    6.2.6 Functions with Varying Parameters

    The new standard provides two primary ways to write a function that takes a varying number of arguments: If all the arguments have the same type, we can pass a library type named initializer_list. If the argument types vary, we can write a special kind of function, known as a variadic template, which we’ll cover in § 16.4 (p. 699).

    initializer_list Parameters

    An initializer_list is a library type that represents an array (§ 3.5, p. 113) of values of the specified type. This type is defined in the initializer_list header.

    Like a vector, initializer_list is a template type (§ 3.3, p. 96). When we define an initializer_list, we must specify the type of the elements that the list will contain:

    initializer_list<string> ls; // initializer_list of strings
    initializer_list<int> li; // initializer_list of ints
    

    Unlike vector, the elements in an initializer_list are always const values.

    We can write our function to produce an error code and error messages from a varying number of arguments as follows:

    void error_msg(ErrCode e, initializer_list<string> il)
    {
        cout << e.msg() << ": ";
        for (const auto &elem : il)
            cout << elem << " " ;
        cout << endl;
    }
    

    To call this function,

    if (expected != actual)
        error_msg(ErrCode(42), {"functionX", expected, actual});
    else
        error_msg(ErrCode(0), {"functionX", "okay"});
    
    Ellipsis Parameters

    Ellipsis parameters are in C++ to allow programs to interface to C code that uses a C library facility named varargs.

    6.3 Return Types and the return Statement

    There are two forms of return statements:

    return; // no return value
    return expression; // return a value
    

    6.3.1 Functions with No Return Value

    A return with no value may be used only in a function that has a return type of void. Functions that return void are not required to contain a return; an implicit return takes place after the function’s last statement.

    6.3.2 Functions That Return a Value

    Every return in a function with a return type other than void must return a value. The value returned must have the same type as the function return type, or it must have a type that can be implicitly converted (§ 4.11, p. 159) to that type.

    How Values Are Returned

    Values are returned in exactly the same way as variables and parameters are initialized: The return value is used to initialize a temporary at the call site, and that temporary is the result of the function call.

    If the return type is a nonreference type, the return value is copied to the call site.

    Never Return a Reference or Pointer to a Local Object

    When a function completes, its storage is freed (§ 6.1.1, p. 204). After a function terminates, references to local objects refer to memory that is no longer valid, and pointers to local objects would point to a nonexistent object.

    // disaster: both returns refer to memory that is no longer available
    const string &manip()
    {
        string ret;
        // transform ret in some way
        if (!ret.empty())
            return ret; // WRONG: returning a reference to a local object!
        else
            return "Empty"; // WRONG: "Empty" is a local temporary string
    }
    

    Tip: One good way to ensure that the return is safe is to ask: To what preexisting object is the reference referring?

    Functions That Return Class Types and the Call Operator
    Reference Returns Are Lvalues

    Calls to functions that return references are lvalues; other return types yield rvalues. We can assign to the result of a function that returns a reference to nonconst:

    char &get_val(string &str, string::size_type ix)
    {
        return str[ix]; // get_val assumes the given index is valid
    }
    
    int main()
    {
        string s("a value");
        get_val(s, 0) = 'A'; // changes s[0] to A
        // ...
        return 0;
    }
    
    List Initializing the Return Value

    Under the new standard, functions can return a braced list of values.

    As an example, the following function returns a vector that holds the error-message strings:

    vector<string> process()
    {
        // ...
        // expected and actual are strings
        if (expected.empty())
            return {}; // return an empty vector
        else if (expected == actual)
            return {"functionX", "okay"}; // return list-initialized vector
        else
            return {"functionX", expected, actual};
    }
    
    Return from main

    There is one exception to the rule that a function with a return type other than void must return a value: If control reaches the end of main and there is no return, then the compiler implicitly inserts a return of 0.

    As we saw in § 1.1 (p. 2), the value returned from main is treated as a status indicator. A zero return indicates success; most other values indicate failure. A nonzero value has a machine-dependent meaning. To make return values machine independent, the cstdlib header defines two preprocessor variables (§ 2.3.2, p. 54) that we can use to indicate success or failure:

    int main()
    {
        if (some_failure)
            return EXIT_FAILURE; // defined in cstdlib
        else
            return EXIT_SUCCESS; // defined in cstdlib
    }
    

    Because these are preprocessor variables, we must not precede them with std::, nor may we mention them in using declarations.

    Recursion

    A function that calls itself, either directly or indirectly, is a recursive function.

    There must always be a path through a recursive function that does not involve a recursive call; otherwise, the function will recurse “forever”, meaning that the function will continue to call itself until the program stack is exhausted.

    Note: The main function may not call itself.

    6.3.3 Returning a Pointer to an Array

    Because we cannot copy an array, a function cannot return an array. However, a function can return a pointer or a reference to an array (§ 3.5.1, p. 114).

    Declaring a Function That Returns a Pointer to an Array

    Remember that

    int arr[10]; // arr is an array of ten ints
    int *p1[10]; // p1 is an array of ten pointers
    int (*p2)[10] = &arr; // p2 points to an array of ten ints
    

    The form of a function that returns a pointer to an array is:

    Type (*function(parameter_list))[dimension]
    

    As an example,

    int (*func(int i))[10];
    

    We can use a type alias to simplify the declaration of func and make it more readable (§ 2.5.1, p. 67):

    typedef int arrT[10]; // arrT is a synonym for the type array of ten ints
    using arrT = int[10]; // equivalent declaration of arrT; see § 2.5.1 (p. 68)
    arrT* func(int i); // func returns a pointer to an array of ten ints
    
    Using a Trailing Return Type

    Under the new standard, another way to simplify the declaration of func is by using a trailing return type.

    // func takes an int argument and returns a pointer to an array of ten ints
    auto func(int i) -> int(*)[10];
    
    Using decltype

    As another alternative, if we know the array(s) to which our function can return a pointer, we can use decltype to declare the return type.

    int odd[] = {1,3,5,7,9};
    int even[] = {0,2,4,6,8};
    // returns a pointer to an array of five int elements
    decltype(odd) *arrPtr(int i)
    {
        return (i % 2) ? &odd : &even; // returns a pointer to the array
    }
    

    6.4 Overloaded Functions

    Functions that have the same name but different parameter lists and that appear in the same scope are overloaded. For example, in § 6.2.4 (p. 214) we defined several functions named print:

    void print(const char *cp);
    void print(const int *beg, const int *end);
    void print(const int ia[], size_t size);
    

    When we call these functions, the compiler can deduce which function we want based on the argument type we pass:

    int j[2] = {0,1};
    print("Hello World"); // calls print(const char*)
    print(j, end(j) - begin(j)); // calls print(const int*, size_t)
    print(begin(j), end(j)); // calls print(const int*, const int*)
    

    Note: The main function may not be overloaded.

    Defining Overloaded Functions
    Determining Whether Two Parameter Types Differ
    Overloading and const Parameters

    As we saw in § 6.2.3 (p. 212), a parameter that has a top-level const is indistinguishable from one without a top-level const, so we cannot overload based on whether the parameter is a top-level const.

    On the other hand, we can overload based on whether the parameter is a reference (or pointer) to the const or nonconst version of a given type; such consts are low-level.

    ADVICE: WHEN NOT TO OVERLOAD A FUNCTION NAME

    We should only overload operations that actually do similar things.

    const_cast and Overloading

    In § 4.11.3 (p. 163) we noted that const_casts are most useful in the context of overloaded functions.

    Calling an Overloaded Function

    6.4.1 Overloading and Scope

    Note: In C++, name lookup happens before type checking.

    6.5 Features for Specialized Uses

    6.5.1 Default Arguments

    If a parameter has a default argument, all the parameters that follow it must also have default arguments.

    typedef string::size_type sz; // typedef see § 2.5.1 (p. 67)
    string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
    
    Calling Functions with Default Arguments
    Default Argument Declarations

    Each parameter can have its default specified only once in a given scope. For example, given

    // no default for the height or width parameters
    string screen(sz, sz, char = ' ');
    

    we cannot change an already declared default value:

    string screen(sz, sz, char = '*'); // error: redeclaration
    

    but we can add a default argument as follows:

    string screen(sz = 24, sz = 80, char); // ok: adds default arguments
    

    Best Practices: Default arguments ordinarily should be specified with the function declaration in an appropriate header.

    Default Argument Initializers

    Local variables may not be used as a default argument.

    // the declarations of wd, def, and ht must appear outside a function
    sz wd = 80;
    char def = ' ';
    sz ht();
    string screen(sz = ht(), sz = wd, char = def);
    string window = screen(); // calls screen(ht(), 80, ' ')
    
    void f2()
    {
        def = '*'; // changes the value of a default argument
        sz wd = 100; // hides the outer definition of wd but does not change the default
        window = screen(); // calls screen(ht(), 80, '*')
    }
    

    Inside f2, we declared a local variable that hides the outer wd. However, the local named wd is unrelated to the default argument passed to screen.

    6.5.2 inline and constexpr Functions

    Calling a function is apt to be slower than evaluating the equivalent expression. On most machines, a function call does a lot of work: Registers are saved before the call and restored after the return; arguments may be copied; and the program branches to a new location.

    inline Functions Avoid Function Call Overhead

    A function specified as inline (usually) is expanded “in line” at each call. If shorterString were defined as inline,

    // inline version: find the shorter of two strings
    inline const string &shorterString(const string &s1, const string &s2)
    {
        return s1.size() <= s2.size() ? s1 : s2;
    }
    

    then this call

    cout << shorterString(s1, s2) << endl;
    

    (probably) would be expanded during compilation into something like

    cout << (s1.size() < s2.size() ? s1 : s2) << endl;
    

    The run-time overhead of making shorterString a function is thus removed.

    Note: The inline specification is only a request to the compiler. The compiler may choose to ignore this request.

    In general, the inline mechanism is meant to optimize small, straight-line functions that are called frequently. Many compilers will not inline a recursive function. A 75-line function will almost surely not be expanded inline.

    constexpr Functions

    A constexpr function is a function that can be used in a constant expression (§ 2.4.4, p. 65).

    Put inline and constexpr Functions in Header Files

    6.5.3 Aids for Debugging

    C++ programmers sometimes use a technique similar to header guards (§ 2.6.3, p. 77) to conditionally execute debugging code. The debugging code is turned off when the application is completed and ready to ship. This approach uses two preprocessor facilities: assert and NDEBUG.

    The assert Preprocessor Macro

    assert is a preprocessor macro. A preprocessor macro is a preprocessor variable that acts somewhat like an inline function.

    assert(expr);
    

    evaluates expr and if the expression is false (i.e., zero), then assert writes a message and terminates the program.

    The assert macro is defined in the cassert header. As we’ve seen, preprocessor names are managed by the preprocessor not the compiler (§ 2.6. 3, p. 76). As a result, we use preprocessor names directly and do not provide a using declaration for them. That is, we refer to assert, not std::assert, and provide no using declaration for assert.

    As with preprocessor variables, macro names must be unique within the program. We should avoid using macro names in our program.

    The NDEBUG Preprocessor Variable

    The behavior of assert depends on the status of a preprocessor variable named NDEBUG. If NDEBUG is defined, assert does nothing. By default, NDEBUG is not defined, so, by default, assert performs a run-time check.

    We can “turn off” debugging by writing a #define NDEBUG at the beginning of the script. Alternatively, most compilers provide a command-line option that lets us define preprocessor variables:

    $ CC -D NDEBUG main.C # use /D with the Microsoft compiler
    

    In addition to using assert, we can write our own conditional debugging code using NDEBUG. If NDEBUG is not defined, the code between the #ifndef and the #endif is executed. If NDEBUG is defined, that code is ignored:

    void print(const int ia[], size_t size)
    {
        // print the name of the function we are debugging
        #ifndef NDEBUG
        // _ _func_ _ is a local static defined by the compiler that holds the function's name
        cerr << _ _func_ _ << ": array size is " << size << endl;
        #endif
        // ...
    

    In addition to _ _func_ _, which the C++ compiler defines, the preprocessor defines four other names that can be useful in debugging:

    • _ _FILE_ _ string literal containing the name of the file
    • _ _LINE_ _ integer literal containing the current line number
    • _ _TIME_ _ string literal containing the time the file was compiled
    • _ _DATE_ _ string literal containing the date the file was compiled

    6.6 Function Matching

    Determining the Candidate and Viable Functions
    Finding the Best Match, If Any

    The closer the types of the argument and parameter are to each other, the better the match.

    Function Matching with Multiple Parameters

    If after looking at each argument there is no single function that is preferable, then the call is in error. The compiler will complain that the call is ambiguous.

    Best Practices: Casts should not be needed to call an overloaded function. The need for a cast suggests that the parameter sets are designed poorly.

    6.6.1 Argument Type Conversions

    Matches Requiring Promotion or Arithmetic Conversion
    Function Matching and const Arguments

    6.7 Pointers to Functions

    A function pointer is a pointer that denotes a function rather than an object. Like any other pointer, a function pointer points to a particular type. A function’s type is determined by its return type and the types of its parameters. For example,

    bool lengthCompare(const string &, const string &);
    

    has type bool(const string&, const string&). To declare a pointer that can point at this function, we declare a pointer in place of the function name:

    // pf points to a function returning bool that takes two const string references
    bool (*pf)(const string &, const string &); // uninitialized
    

    Starting from the name we are declaring, we see that pf is preceded by a *, so pf is a pointer. To the right is a parameter list, which means that pf points to a function. Looking left, we find that the type the function returns is bool.

    Using Function Pointers

    When we use the name of a function as a value, the function is automatically converted to a pointer. For example, we can assign the address of lengthCompare to pf as follows:

    pf = lengthCompare; // pf now points to the function named lengthCompare
    pf = &lengthCompare; // equivalent assignment: address-of operator is optional
    

    We can use a pointer to a function to call the function. We can do so directly—there is no need to dereference the pointer:

    bool b1 = pf("hello", "goodbye"); // calls lengthCompare
    bool b2 = (*pf)("hello", "goodbye"); // equivalent call
    bool b3 = lengthCompare("hello", "goodbye"); // equivalent call
    

    There is no conversion between pointers to one function type and pointers to another function type. However, as usual, we can assign nullptr (§ 2.3.2, p. 53) or a zero-valued integer constant expression to a function pointer to indicate that the pointer does not point to any function:

    string::size_type sumLength(const string&, const string&);
    bool cstringCompare(const char*, const char*);
    pf = 0; // ok: pf points to no function
    pf = sumLength; // error: return type differs
    pf = cstringCompare; // error: parameter types differ
    pf = lengthCompare; // ok: function and pointer types match exactly
    
    Pointers to Overloaded Functions

    The compiler uses the type of the pointer to determine which overloaded function to use.

    Function Pointer Parameters

    Just as with arrays (§ 6.2.4, p. 214), we cannot define parameters of function type but can have a parameter that is a pointer to function. As with arrays, we can write a parameter that looks like a function type, but it will be treated as a pointer:

    // third parameter is a function type and is automatically treated as a pointer to function
    void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
    // equivalent declaration: explicitly define the parameter as a pointer to function
    void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));
    

    When we pass a function as an argument, we can do so directly. It will be automatically converted to a pointer:

    // automatically converts the function lengthCompare to a pointer to function
    useBigger(s1, s2, lengthCompare);
    

    As we’ve just seen in the declaration of useBigger, writing function pointer types quickly gets tedious. Type aliases (§ 2.5.1, p. 67), along with decltype (§ 2.5.3, p. 70), let us simplify code that uses function pointers:

    // Func and Func2 have function type
    typedef bool Func(const string&, const string&);
    typedef decltype(lengthCompare) Func2; // equivalent type
    // FuncP and FuncP2 have pointer to function type
    typedef bool(*FuncP)(const string&, const string&);
    typedef decltype(lengthCompare) *FuncP2; // equivalent type
    

    It is important to note that decltype returns the function type; the automatic conversion to pointer is not done, so we must add the * if we want a pointer. We can redeclare useBigger using any of these types:

    // equivalent declarations of useBigger using type aliases
    void useBigger(const string&, const string&, Func);
    void useBigger(const string&, const string&, FuncP2);
    

    In the first case, the compiler will automatically convert the function type represented by Func to a pointer.

    Returning a Pointer to Function

    As with arrays (§ 6.3.3, p. 228), we can’t return a function type but can return a pointer to a function type. Similarly, we must write the return type as a pointer type; the compiler will not automatically treat a function return type as the corresponding pointer type.

    For example,

    // the return type of f1 is a pointer to a function that has type int(int)
    int (*f1(int))(int*, int);
    

    Reading this declaration from the inside out, we see that f1 has a parameter list, so f1 is a function. f1 is preceded by a * so f1 returns a pointer. The type of that pointer itself has a parameter list, so the pointer points to a function. That function takes an int parameter and returns an int.

    Also as with array returns, we can simply the declaration by using a type alias:

    using F = int(int*, int); // F is a function type, not a pointer
    using PF = int(*)(int*, int); // PF is a pointer type
    
    PF f1(int); // ok: PF is a pointer to function; f1 returns a pointer to function
    F f1(int); // error: F is a function type; f1 can't return a function
    F* f1(int); // ok: explicitly specify that the return type is a pointer to function
    

    For completeness, it’s worth noting that we can simplify declarations of functions that return pointers to function by using a trailing return (§ 6.3.3, p. 229):

    auto f1(int) -> int (*)(int*, int);
    
    Using auto or decltype for Function Pointer Types

    If we know which function(s) we want to return, we can use decltype to simplify writing a function pointer return type. For example, assume we have two functions, both of which return a string::size_type and have two const string& parameters. We can write a third function that takes a string parameter and returns a pointer to one of these two functions as follows:

    string::size_type sumLength(const string&, const string&);
    string::size_type largerLength(const string&, const string&);
    // depending on the value of its string parameter,
    // getFcn returns a pointer to sumLength or to largerLength
    decltype(sumLength) *getFcn(const string &);
    

    Chapter Summary

    Functions are named units of computation. Every function has a return type, a name, a (possibly empty) list of parameters, and a function body.

    In C++, functions may be overloaded: The same name may be used to define different functions as long as the number or types of the parameters in the functions differ. The compiler automatically figures out which function to call based on the arguments in a call. The process of selecting the right function from a set of overloaded functions is referred to as function matching.

    Defined Terms

    Almost all terms are important. Read p. 251~252 for detail.


    References

    Lippman, Stanley B., Josée Lajoie, and Barbara E. Moo. C++ Primer. Addison-Wesley Professional, 2012.



    Comments