Team LiB
Previous Section Next Section

14.8. Function-Call Operator

 
Image

Classes that overload the call operator allow objects of its type to be used as if they were a function. Because such classes can also store state, they can be more flexible than ordinary functions.

 

As a simple example, the following struct, named absInt, has a call operator that returns the absolute value of its argument:

 

 

struct absInt {
    int operator()(int val) const {
        return val < 0 ? -val : val;
    }
};

 

This class defines a single operation: the function-call operator. That operator takes an argument of type int and returns the argument’s absolute value.

 

We use the call operator by applying an argument list to an absInt object in a way that looks like a function call:

 

 

int i = -42;
absInt absObj;      // object that has a function-call operator
int ui = absObj(i); // passes i to absObj.operator()

 

Even though absObj is an object, not a function, we can “call” this object. Calling an object runs its overloaded call operator. In this case, that operator takes an int value and returns its absolute value.

 

Image Note

The function-call operator must be a member function. A class may define multiple versions of the call operator, each of which must differ as to the number or types of their parameters.

 

 

Objects of classes that define the call operator are referred to as function objects. Such objects “act like functions” because we can call them.

 

Function-Object Classes with State

 

Like any other class, a function-object class can have additional members aside from operator(). Function-object classes often contain data members that are used to customize the operations in the call operator.

 

As an example, we’ll define a class that prints a string argument. By default, our class will write to cout and will print a space following each string. We’ll also let users of our class provide a different stream on which to write and provide a different separator. We can define this class as follows:

 

 

class PrintString {
public:
    PrintString(ostream &o = cout, char c = ' '):
        os(o), sep(c) { }
    void operator()(const string &s) const { os << s << sep; }
private:
    ostream &os;   // stream on which to write
    char sep;      // character to print after each output
};

 

Our class has a constructor that takes a reference to an output stream and a character to use as the separator. It uses cout and a space as default arguments (§ 6.5.1, p. 236) for these parameters. The body of the function-call operator uses these members when it prints the given string.

 

When we define PrintString objects, we can use the defaults or supply our own values for the separator or output stream:

 

 

PrintString printer;   // uses the defaults; prints to cout
printer(s);            // prints s followed by a space on cout
PrintString errors(cerr, '\n');
errors(s);             // prints s followed by a newline on cerr

 

Function objects are most often used as arguments to the generic algorithms. For example, we can use the library for_each algorithm (§ 10.3.2, p. 391) and our PrintString class to print the contents of a container:

 

 

for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));

 

The third argument to for_each is a temporary object of type PrintString that we initialize from cerr and a newline character. The call to for_each will print each element in vs to cerr followed by a newline.

 

Exercises Section 14.8

 

Exercise 14.33: How many operands may an overloaded function-call operator take?

Exercise 14.34: Define a function-object class to perform an if-then-else operation: The call operator for this class should take three parameters. It should test its first parameter and if that test succeeds, it should return its second parameter; otherwise, it should return its third parameter.

Exercise 14.35: Write a class like PrintString that reads a line of input from an istream and returns a string representing what was read. If the read fails, return the empty string.

Exercise 14.36: Use the class from the previous exercise to read the standard input, storing each line as an element in a vector.

Exercise 14.37: Write a class that tests whether two values are equal. Use that object and the library algorithms to write a program to replace all instances of a given value in a sequence.


 

14.8.1. Lambdas Are Function Objects

 

In the previous section, we used a PrintString object as an argument in a call to for_each. This usage is similar to the programs we wrote in § 10.3.2 (p. 388) that used lambda expressions. When we write a lambda, the compiler translates that expression into an unnamed object of an unnamed class (§ 10.3.3, p. 392). The classes generated from a lambda contain an overloaded function-call operator. For example, the lambda that we passed as the last argument to stable_sort:

 

 

// sort words by size, but maintain alphabetical order for words of the same size
stable_sort(words.begin(), words.end(),
            [](const string &a, const string &b)
              { return a.size() < b.size();});

 

acts like an unnamed object of a class that would look something like

 

 

class ShorterString {
public:
    bool operator()(const string &s1, const string &s2) const
    { return s1.size() < s2.size(); }
};

 

The generated class has a single member, which is a function-call operator that takes two strings and compares their lengths. The parameter list and function body are the same as the lambda. As we saw in § 10.3.3 (p. 395), by default, lambdas may not change their captured variables. As a result, by default, the function-call operator in a class generated from a lambda is a const member function. If the lambda is declared as mutable, then the call operator is not const.

 

We can rewrite the call to stable_sort to use this class instead of the lambda expression:

 

 

stable_sort(words.begin(), words.end(), ShorterString());

 

The third argument is a newly constructed ShorterString object. The code in stable_sort will “call” this object each time it compares two strings. When the object is called, it will execute the body of its call operator, returning true if the first string’s size is less than the second’s.

 
Classes Representing Lambdas with Captures
 

As we’ve seen, when a lambda captures a variable by reference, it is up to the program to ensure that the variable to which the reference refers exists when the lambda is executed (§ 10.3.3, p. 393). Therefore, the compiler is permitted to use the reference directly without storing that reference as a data member in the generated class.

 

In contrast, variables that are captured by value are copied into the lambda (§ 10.3.3, p. 392). As a result, classes generated from lambdas that capture variables by value have data members corresponding to each such variable. These classes also have a constructor to initialize these data members from the value of the captured variables. As an example, in § 10.3.2 (p. 390), the lambda that we used to find the first string whose length was greater than or equal to a given bound:

 

 

// get an iterator to the first element whose size() is >= sz
auto wc = find_if(words.begin(), words.end(),
            [sz](const string &a)

 

would generate a class that looks something like

 

 

class SizeComp {
    SizeComp(size_t n): sz(n) { } // parameter for each captured variable
    // call operator with the same return type, parameters, and body as the lambda
    bool operator()(const string &s) const
        { return s.size() >= sz; }
private:
    size_t sz; // a data member for each variable captured by value
};

 

Unlike our ShorterString class, this class has a data member and a constructor to initialize that member. This synthesized class does not have a default constructor; to use this class, we must pass an argument:

 

 

// get an iterator to the first element whose size() is >= sz
auto wc = find_if(words.begin(), words.end(), SizeComp(sz));

 

Classes generated from a lambda expression have a deleted default constructor, deleted assignment operators, and a default destructor. Whether the class has a defaulted or deleted copy/move constructor depends in the usual ways on the types of the captured data members (§ 13.1.6, p. 508, and § 13.6.2, p. 537).

 

Exercises Section 14.8.1

 

Exercise 14.38: Write a class that tests whether the length of a given string matches a given bound. Use that object to write a program to report how many words in an input file are of sizes 1 through 10 inclusive.

Exercise 14.39: Revise the previous program to report the count of words that are sizes 1 through 9 and 10 or more.

Exercise 14.40: Rewrite the biggies function from § 10.3.2 (p. 391) to use function-object classes in place of lambdas.

 

Exercise 14.41: Why do you suppose the new standard added lambdas? Explain when you would use a lambda and when you would write a class instead.


 

14.8.2. Library-Defined Function Objects

 

The standard library defines a set of classes that represent the arithmetic, relational, and logical operators. Each class defines a call operator that applies the named operation. For example, the plus class has a function-call operator that applies + to a pair of operands; the modulus class defines a call operator that applies the binary % operator; the equal_to class applies ==; and so on.

 

These classes are templates to which we supply a single type. That type specifies the parameter type for the call operator. For example, plus<string> applies the string addition operator to string objects; for plus<int> the operands are ints; plus<Sales_data> applies + to Sales_datas; and so on:

 

 

plus<int> intAdd;       // function object that can add two int values
negate<int> intNegate;  // function object that can negate an int value
// uses intAdd::operator(int, int) to add 10 and 20
int sum = intAdd(10, 20);         // equivalent to sum = 30
sum = intNegate(intAdd(10, 20));  // equivalent to sum = 30
// uses intNegate::operator(int) to generate -10 as the second parameter
// to intAdd::operator(int, int)
sum = intAdd(10, intNegate(10));  // sum = 0

 

These types, listed in Table 14.2, are defined in the functional header.

 

Table 14.2. Library Function Objects

 
Image
 
Using a Library Function Object with the Algorithms
 

The function-object classes that represent operators are often used to override the default operator used by an algorithm. As we’ve seen, by default, the sorting algorithms use operator<, which ordinarily sorts the sequence into ascending order. To sort into descending order, we can pass an object of type greater. That class generates a call operator that invokes the greater-than operator of the underlying element type. For example, if svec is a vector<string>,

 

 

// passes a temporary function object that applies the < operator to two strings
sort(svec.begin(), svec.end(), greater<string>());

 

sorts the vector in descending order. The third argument is an unnamed object of type greater<string>. When sort compares elements, rather than applying the < operator for the element type, it will call the given greater function object. That object applies > to the string elements.

 

One important aspect of these library function objects is that the library guarantees that they will work for pointers. Recall that comparing two unrelated pointers is undefined (§ 3.5.3, p. 120). However, we might want to sort a vector of pointers based on their addresses in memory. Although it would be undefined for us to do so directly, we can do so through one of the library function objects:

 

 

vector<string *> nameTable;  // vector of pointers
// error: the pointers in nameTable are unrelated, so < is undefined
sort(nameTable.begin(), nameTable.end(),
     [](string *a, string *b) { return a < b; });
// ok: library guarantees that less on pointer types is well defined
sort(nameTable.begin(), nameTable.end(), less<string*>());

 

It is also worth noting that the associative containers use less<key_type> to order their elements. As a result, we can define a set of pointers or use a pointer as the key in a map without specifying less directly.

 

Exercises Section 14.8.2

 

Exercise 14.42: Using library function objects and adaptors, define an expression to

(a) Count the number of values that are greater than 1024

 

(b) Find the first string that is not equal to pooh

 

(c) Multiply all values by 2

 

Exercise 14.43: Using library function objects, determine whether a given int value is divisible by any element in a container of ints.


 

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 div {
    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 div 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 = div();  // 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
    {"/",  div()},               // 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 div 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.


 
Team LiB
Previous Section Next Section