14.8.3. Callable Objects and function

C++ has several kinds of callable objects: functions and pointers to functions, lambdas (§ 10.3.2, p. 388), objects created by bind10.3.4, p. 397), and classes that overload the function-call operator.

Like any other object, a callable object has a type. For example, each lambda has its own unique (unnamed) class type. Function and function-pointer types vary by their return type and argument types, and so on.

However, two callable objects with different types may share the same call signature. The call signature specifies the type returned by a call to the object and the argument type(s) that must be passed in the call. A call signature corresponds to a function type. For example:

int(int, int)

is a function type that takes two ints and returns an int.

Different Types Can Have the Same Call Signature

Sometimes we want to treat several callable objects that share a call signature as if they had the same type. For example, consider the following different types of callable objects:

// ordinary function
int add(int i, int j) { return i + j; }
// lambda, which generates an unnamed function-object class
auto mod = [](int i, int j) { return i % j; };
// function-object class
struct divide {
    int operator()(int denominator, int divisor) {
        return denominator / divisor;
    }
};

Each of these callables applies an arithmetic operation to its parameters. Even though each has a distinct type, they all share the same call signature:

int(int, int)

We might want to use these callables to build a simple desk calculator. To do so, we’d want to define a function table to store “pointers” to these callables. When the program needs to execute a particular operation, it will look in the table to find which function to call.

In C++, function tables are easy to implement using a map. In this case, we’ll use a string corresponding to an operator symbol as the key; the value will be the function that implements that operator. When we want to evaluate a given operator, we’ll index the map with that operator and call the resulting element.

If all our functions were freestanding functions, and assuming we were handling only binary operators for type int, we could define the map as

// maps an operator to a pointer to a function taking two ints and returning an int
map<string, int(*)(int,int)> binops;

We could put a pointer to add into binops as follows:

// ok: add is a pointer to function of the appropriate type
binops.insert({"+", add}); // {"+", add} is a pair § 11.2.3 (p. 426)

However, we can’t store mod or an object of type divide in binops:

binops.insert({"%", mod}); // error: mod is not a pointer to function

The problem is that mod is a lambda, and each lambda has its own class type. That type does not match the type of the values stored in binops.

The Library function Type

We can solve this problem using a new library type named function that is defined in the functional header; Table 14.3 (p. 579) lists the operations defined by function.

Table 14.3. Operations on function

Image
Image

function is a template. As with other templates we’ve used, we must specify additional information when we create a function type. In this case, that information is the call signature of the objects that this particular function type can represent. As with other templates, we specify the type inside angle brackets:

function<int(int, int)>

Here we’ve declared a function type that can represent callable objects that return an int result and have two int parameters. We can use that type to represent any of our desk calculator types:

function<int(int, int)> f1 = add;    // function pointer
function<int(int, int)> f2 = divide();  // object of a function-object class
function<int(int, int)> f3 = [](int  i, int j) // lambda
                             { return i * j; };
cout << f1(4,2) << endl; // prints 6
cout << f2(4,2) << endl; // prints 2
cout << f3(4,2) << endl; // prints 8

We can now redefine our map using this function type:

// table of callable objects corresponding to each binary operator
// all the callables must take two ints and return an int
// an element can be a function pointer, function object, or lambda
map<string, function<int(int, int)>> binops;

We can add each of our callable objects, be they function pointers, lambdas, or function objects, to this map:

map<string, function<int(int, int)>> binops = {
    {"+", add},                  // function pointer
    {"-", std::minus<int>()},    // library function object
    {"/",  divide()},               // user-defined function object
    {"*", [](int i, int j) { return i * j; }}, // unnamed lambda
    {"%", mod} };                // named lambda object

Our map has five elements. Although the underlying callable objects all have different types from one another, we can store each of these distinct types in the common function<int(int, int)> type.

As usual, when we index a map, we get a reference to the associated value. When we index binops, we get a reference to an object of type function. The function type overloads the call operator. That call operator takes its own arguments and passes them along to its stored callable object:

binops["+"](10, 5); // calls add(10, 5)
binops["-"](10, 5); // uses the call operator of the minus<int> object
binops["/"](10, 5); // uses the call operator of the divide object
binops["*"](10, 5); // calls the lambda function object
binops["%"](10, 5); // calls the lambda function object

Here we call each of the operations stored in binops. In the first call, the element we get back holds a function pointer that points to our add function. Calling binops["+"](10, 5) uses that pointer to call add, passing it the values 10 and 5. In the next call, binops["-"], returns a function that stores an object of type std::minus<int>. We call that object’s call operator, and so on.

Overloaded Functions and function

We cannot (directly) store the name of an overloaded function in an object of type function:

int add(int i, int j) { return i + j; }
Sales_data add(const Sales_data&, const Sales_data&);
map<string, function<int(int, int)>> binops;
binops.insert( {"+", add} ); // error: which add?

One way to resolve the ambiguity is to store a function pointer (§ 6.7, p. 247) instead of the name of the function:

int (*fp)(int,int) = add; // pointer to the version of add that takes two ints
binops.insert( {"+", fp} ); // ok: fp points to the right version of add

Alternatively, we can use a lambda to disambiguate:

// ok: use a lambda to disambiguate which version of add we want to use
binops.insert( {"+", [](int a, int b) {return add(a, b);} } );

The call inside the lambda body passes two ints. That call can match only the version of add that takes two ints, and so that is the function that is called when the lambda is executed.


Image Note

The function class in the new library is not related to classes named unary_function and binary_function that were part of earlier versions of the library. These classes have been deprecated by the more general bind function (§ 10.3.4, p. 401).



Exercises Section 14.8.3

Exercise 14.44: Write your own version of a simple desk calculator that can handle binary operations.


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

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