Variations on the Theme of Function Pointers

With function pointers, the notation can get intimidating. Let’s look at an example that illustrates some of the challenges of function pointers and ways of dealing with them. To begin, here are prototypes for some functions that share the same signature and return type:

const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);

The signatures might look different, but they are the same. First, recall that in a function prototype parameter list const double ar[] and const double * ar have exactly the same meaning. Second, recall that in a prototype you can omit identifiers. Therefore, const double ar[] can be reduced to const double [], and const double * ar can be reduced to const double *. So all the function signatures shown previously have the same meaning. Function definitions, on the other hand, do provide identifiers, so either const double ar[] or const double * ar will serve in that context.

Next, suppose you wish to declare a pointer that can point to one of these three functions. The technique, you’ll recall, is if pa is the desired pointer, take the prototype for a target function and replace the function name with (*pa):

const double * (*p1)(const double *, int);

This can be combined with initialization:

const double * (*p1)(const double *, int) = f1;

With the C++11 automatic type deduction feature, you can simplify this a bit:

auto p2 = f2;  // C++11 automatic type deduction

Now consider the following statements:

cout <<  (*p1)(av,3) << ": " << *(*p1)(av,3) << endl;
cout << p2(av,3) << ": " << *p2(av,3) << endl;

Both (*p1)(av,3) and p2(av,3), recall, represent calling the pointed-to functions (f1() and f2(), in this case) with av and 3 as arguments. Therefore, what should print are the return values of these two functions. The return values are type const double * (that is, address of double values). So the first part of each cout expression should print the address of a double value. To see the actual value stored at the addresses, we need to apply the * operator to these addresses, and that’s what the expressions *(*p1)(av,3) and *p2(av,3) do.

With three functions to work with, it could be handy to have an array of function pointers. Then one can use a for loop to call each function, via its pointer, in turn. What would that look like? Clearly, it should look something like the declaration of a single pointer, but there should be a [3] somewhere to indicate an array of three pointers. The question is where. And here’s the answer (including initialization):

const double * (*pa[3])(const double *, int) = {f1,f2,f3};

Why put the [3] there? Well, pa is an array of three things, and the starting point for declaring an array of three things is this: pa[3]. The rest of the declaration is about what kind of thing is to be placed in the array. Operator precedence ranks [] higher than *, so *pa[3] says pa is an array of three pointers. The rest of the declaration indicates what each pointer points to: a function with a signature of const double *, int and a return type of const double *. Hence, pa is an array of three pointers, each of which is a pointer to a function that takes a const double * and int as arguments and returns a const double *.

Can we use auto here? No. Automatic type deduction works with a single initializer value, not an initialization list. But now that we have the array pa, it is simple to declare a pointer of the matching type:

auto pb = pa;

The name of an array, as you’ll recall, is a pointer to its first element, so both pa and pb are pointers to a pointer to a function.

How can we use them to call a function? Both pa[i] and pb[i] represent pointers in the array, so you can use either of the function call notations with either of them:

const double * px = pa[0](av,3);
const double * py = (*pb[1])(av,3);

And you can apply the * operator to get the pointed-to double value:

double x = *pa[0](av,3);
double y = *(*pb[1])(av,3);

Something else you can do (and who wouldn’t want to?) is create a pointer to the whole array. Because the array name pa already is a pointer to a function pointer, a pointer to the array would be a pointer to a pointer to a pointer. This sounds intimidating, but because the result can be initialed with a single value, you can use auto:

auto pc = &pa;  // C++11 automatic type deduction

What if you prefer to do it yourself? Clearly, the declaration should resemble the declaration for pa, but because there is one more level of indirection, we’ll need one more * stuck somewhere. In particular, if we call the new pointer pd, we need to indicate that it is pointer, not an array name. This suggests the heart of the declaration should be (*pd)[3]. The parentheses bind the pd identifier to the *:

*pd[3]    // an array of 3 pointers
(*pd)[3]  // a pointer to an array of 3 elements

In other words, pd is a pointer, and it points to an array of three things. What those things are is described by the rest of the original declaration of pa. This approach yields the following:

const double *(*(*pd)[3])(const double *, int) = &pa;

To call a function, realize that if pd points to an array, then *pd is the array and (*pd)[i] is an array element, which is a pointer to a function. The simpler notation, then, for the function call is (*pd)[i](av,3), and *(*pd)[i](av,3) would be the value that the returned pointer points to. Alternatively, you could use second syntax for invoking a function with a pointer and use (*(*pd)[i])(av,3) for the call and *(*(*pd)[i])(av,3) for the pointed-to double value.

Be aware of the difference between pa, which as an array name is an address, and &pa. As you’ve seen before, in most contexts pa is the address of the first element of the array—that is, &pa[0]. Therefore, it is the address of a single pointer. But &pa is the address of the entire array (that is, of a block of three pointers). Numerically, pa and &pa may have the same value, but they are of different types. One practical difference is that pa+1 is the address of the next element in the array, whereas &pa+1 is the address of the next block of 12 bytes (assuming addresses are 4 bytes) following the pa array. Another difference is that you dereference pa once to get the value of the first element and you deference &pa twice to get the same value:

**&pa == *pa == pa[0]

Listing 7.19 puts this discussion to use. For illustrative purposes, the functions f1(), and so on, have been kept embarrassingly simple. The program shows, as comments, the C++98 alternatives to using auto.

Listing 7.19. arfupt.cpp


// arfupt.cpp -- an array of function pointers
#include <iostream>
// various notations, same signatures
const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);

int main()
{
    using namespace std;
    double av[3] = {1112.3, 1542.6, 2227.9};

    // pointer to a function
    const double *(*p1)(const double *, int) = f1;
    auto p2 = f2;  // C++11 automatic type deduction
    // pre-C++11 can use the following code instead
    // const double *(*p2)(const double *, int) = f2;
    cout << "Using pointers to functions: ";
    cout << " Address  Value ";
    cout <<  (*p1)(av,3) << ": " << *(*p1)(av,3) << endl;
    cout << p2(av,3) << ": " << *p2(av,3) << endl;

    // pa an array of pointers
    // auto doesn't work with list initialization
    const double *(*pa[3])(const double *, int) = {f1,f2,f3};
    // but it does work for initializing to a single value
    // pb a pointer to first element of pa
    auto pb = pa;
    // pre-C++11 can use the following code instead
    // const double *(**pb)(const double *, int) = pa;
    cout << " Using an array of pointers to functions: ";
    cout << " Address  Value ";
    for (int i = 0; i < 3; i++)
        cout << pa[i](av,3) << ": " << *pa[i](av,3) << endl;
    cout << " Using a pointer to a pointer to a function: ";
    cout << " Address  Value ";
    for (int i = 0; i < 3; i++)
        cout << pb[i](av,3) << ": " << *pb[i](av,3) << endl;

    // what about a pointer to an array of function pointers
    cout << " Using pointers to an array of pointers: ";
    cout << " Address  Value ";
    // easy way to declare pc
    auto pc = &pa;
    // pre-C++11 can use the following code instead
    // const double *(*(*pc)[3])(const double *, int) = &pa;
    cout << (*pc)[0](av,3) << ": " << *(*pc)[0](av,3) << endl;
    // hard way to declare pd
    const double *(*(*pd)[3])(const double *, int) = &pa;
    // store return value in pdb
    const double * pdb = (*pd)[1](av,3);
    cout << pdb << ": " << *pdb << endl;
    // alternative notation
    cout << (*(*pd)[2])(av,3) << ": " << *(*(*pd)[2])(av,3) << endl;
    // cin.get();
    return 0;
}

// some rather dull functions

const double * f1(const double * ar, int n)
{
    return ar;
}
const double * f2(const double ar[], int n)
{
    return ar+1;
}
const double * f3(const double ar[], int n)
{
    return ar+2;
}


And here is the output:

Using pointers to functions:
 Address  Value
002AF9E0: 1112.3
002AF9E8: 1542.6

Using an array of pointers to functions:
 Address  Value
002AF9E0: 1112.3
002AF9E8: 1542.6
002AF9F0: 2227.9

Using a pointer to a pointer to a function:
 Address  Value
002AF9E0: 1112.3
002AF9E8: 1542.6
002AF9F0: 2227.9

Using pointers to an array of pointers:
 Address  Value
002AF9E0: 1112.3
002AF9E8: 1542.6
002AF9F0: 2227.9

The addresses shown are the locations of the double values in the av array.

This example may seem esoteric, but pointers to arrays of pointers to functions are not unheard of. Indeed, the usual implementation of virtual class methods (see Chapter 13, “Class Inheritance”) uses this technique. Fortunately, the compiler handles the details.

..................Content has been hidden....................

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