14.8. Function-Call Operator
FundamentalClasses 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.
INFO
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.
INFO
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 string
s 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 string
s. 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).
INFO
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 int
s; plus<Sales_data>
applies +
to Sales_data
s; 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
Arithmetic | Relational | Logical |
---|---|---|
plus<Type> | equal_to<Type> | logical_and<Type> |
minus<Type> | not_equal_to<Type> | logical_or<Type> |
multiplies<Type> | greater<Type> | logical_not<Type> |
divides<Type> | greater_equal<Type> | |
modulus<Type> | less<Type> | |
negate<Type> | less_equal<Type> |
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.
INFO
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 int
s.
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 bind
(§ 10.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 int
s 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
Code | Description |
---|---|
function<T> f; | f is a null function object that can store callable objects with a call signature that is equivalent to the function type T (i.e., T is result_type(args) ). |
function<T> f(nullptr); | Explicitly construct a null function . |
function<T> f(obj); | Stores a copy of the callable object obj in f . |
f | Use f as a condition; true if f holds a callable object; false otherwise. |
f(args) | Calls the object in f passing args . |
Types defined as members of function<T>
:
Type | Description |
---|---|
result_type | The type returned by this function type's callable object. |
argument_type first_argument_type second_argument_type | Types defined when T has exactly one or two arguments. If T has one argument, argument_type is a synonym for that type. If it has two arguments, first_argument_type and second_argument_type are synonyms for those argument types. |
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 int
s. That call can match only the version of add
that takes two int
s, and so that is the function that is called when the lambda is executed.
INFO
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).
INFO
Exercises Section 14.8.3
Exercise 14.44: Write your own version of a simple desk calculator that can handle binary operations.