C + + overloaded function call operator, "behave like a function"

Posted by akreation on Fri, 03 Sep 2021 19:52:15 +0200

If we overload the function call operator, we can use the object of this class as if we were using a function. Because such classes can also store state, they are more flexible than ordinary functions
For a simple example, the following struct named absInt contains a calling operator that returns the absolute value of its parameters

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

This class only defines one: the function call operator, which is responsible for accepting an argument of type int and then returning the absolute value of the argument.
We use another absInt object of the calling operator to act on an argument list. This process seems to want to call the function very much

int i=-42;
absInt=absObj;
int ui=absInt(j)

Even if absInt is an object rather than a function, we can "call" the object. Calling this object is actually an overloaded calling operator.
If a class defines a call operator, the objects of the class are called function objects. Because such objects can be called, we say that these objects "behave like functions"

Object class with state

Like other classes, function object classes can contain other member functions in addition to operator(). Function object classes usually contain primary data members, and these constant sources are used to customize the operation of calling operators
For example, we will define a class that prints the contents of string arguments. In this case, our class will write the content to cout, and each string is separated by a space. It also allows users of the class to provide other writable streams and other separators. We 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;}
public:
	ostream &os;//Destination stream for writing
	char sep;//A character used to separate different outputs
}

When defining the printString object, we can either use the default value or provide our own value for the separator and output stream:

PrintString printer;
printString(s);
PrintString errors(cerror, '\n');
errors(s);

The function object is used as the argument of the generic algorithm. For example, you can use the standard library for_each algorithm and our own PrintString class to print containers

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

for_ The third argument of each is a temporary object of type PrintString, in which we initialize the modified object with cerr and newline character. When the program calls for_ During each, each element in vs will be printed to cerr at one time, with line breaks between elements

lambda is a function object

When we write a lambda, the compiler translates the expression into an unnamed object of an unnamed class. The class generated by lambda expression contains an overloaded function call operator. For example, for the class we pass to stable_ Lambda expression with sort as the last argument

//The words are sorted according to their length, and the words with the same length are arranged in alphabetical order
stable_sort(words.begin(),words.end(),[](const string &a,const string &b)
{return a.size()<b.size();})

Its behavior is similar to a named object of the following class

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

After replacing the lambda expression with this class, we can override and call stable_sort;

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

Class representing lambda and corresponding capture behavior

As we know, when a lambda expression captures variables by reference, it is the responsibility of the program to ensure that the object referenced during lambda execution does exist. Therefore, the compiler can use this reference directly without storing it as a non data member in the lambda generated class
Instead, variables captured by values are copied into lambda. Therefore, the class generated by this lambda must establish the corresponding data member for the variable with each value, and create a constructor to initialize the data member with the value of the captured variable. For example, find the first string object whose length is less than the given value

//Get the first iterator pointing to the element satisfying the condition, which satisfies size () is > = Sz
auto wc=find_if(words.begin(),words.end(),[sz](const string &a){return a.size()>sz;})

The class generated by this lambda expression will look like

class SizeComp{
	SizeComp(size_t n):sz(n){}//This parameter corresponds to the captured variable
	//This variable is consistent with the return type and formal parameters of the operator and function body Betta lambda
	bool operator()(const string &s){return s.size()>=sz;}
private:
	size_t sz;
}

Unlike our ShorterString class, the above class contains a data member and a constructor to initialize the member. This composite class does not contain a default constructor because you must provide an argument to use this class

//Get the first iterator that points to the condition, and the element satisfies size () is > = Sz
auto wc=find_if(words.begin(),words.end(),SizeComp(sz));

Function object defined by standard library

The standard library defines a group of classes representing arithmetic operators, relational operators and logical operators. Each class defines a calling operator to perform naming operations. For example, plus defines a function call operator, which is used to perform a + operation on an operator object; The module class calls an operator to perform binary% operations; equal_ The to class executes = =, and so on.
These classes are defined in the form of templates. We can specify specific application types for them. The call here is the formal parameter type of the call operator. For example, plus < string > makes the string addition operator act on the string object; The operand of plus < int > is int; Plus<Sales_data > for sales_ The data object performs an addition operation, and so on

plus<int> intAdd;
negate<int> intNegate;
//Use intAdd::operator(int, int)
int sum=intAdd(10,20);
sum=intNegate(intAdd(10,20));
sum=intAdd(10,intNegate(10));

Using standard library function objects in algorithms

The function object class representing the operator is often used to replace the default operator in the algorithm. As we know, by default, the permutation algorithm uses operator < to arrange the series in ascending order. If we want to perform the normalization of descending order, we pass in an object of type greater. This class will generate a calling operator and be responsible for performing the greater than operation of the type to be sorted. For example, if svec is a vector < string >\

sort(svec.begin().svec.end().greater<string>());

In particular, the standard library specifies that its function objects also use pointers. We have described before that comparing two unrelated pointers will produce a defined behavior. However, we might want to sort the vector of the pointer by comparing the memory address of the pointer. Doing so directly will result in undefined behavior, so we can use a standard library function to achieve this

Callable objects and function s

C + + language there are several callable objects in C + + Language: functions, function pointers, lambda expressions, objects created by bind, and classes that overload function call operators
Like other objects, callable objects have types. For example, each lambda has its own unique class type; The type of a function, that is, a function pointer, is determined by its return type and arguments
However, two different types of callable objects may share the same call form. The call form indicates the type returned by the call and the arguments passed to the call. A call form corresponds to a function type, for example

int add(int,int)

Is a function type that accepts two ints and returns an int

Different types may have the same call form

//Ordinary function
int add(int i,int j){return i+j;
//lambda, which produces 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}}
}

The above callable functions perform different sun tree operations on their parameters. Although their types are different, they share the same call form;

int add(int,int)

We might want to use these callable objects to build a simple desktop calculator. To achieve this, you need to define a function table to store "pointers" to these callable objects. When the program needs to perform a specific operation, look up the calling function from the table.
In C + + language, function table is easy to implement through map. For this example, we use a string object representing the operation symbol as the keyword; The function that implements the operator is used as the value. When we need the value of a given operator, we first index map through the operator and then call the function we found.
Assuming that all our functions are independent of each other and only handle binary operations on int, map can be defined as follows:

map<string,int(*)(int,int)>binops;

We can add the pointer of add to binop in the following form;

bimop.insert({"+",add});

But we can't save mod or divide into binops

Standard library function

We can use a new standard library called function to solve the above problem. Function is defined in the functional header file

  • Function < T > f: f is an empty functional used to store callable objects. The call form of these callable objects should be the same as function type T
  • Function < T > F (nullptr): construct an empty function explicitly
  • Function < T > f (obj): store a copy of object obj in f
  • f: Take F as a condition; True when f contains a callable object; Otherwise, it is false
  • f(args): the object that calls f, and the parameter is args
    Member type defined as function < T >
  • result_type: the type returned by the callable object of this function type
  • Type defined when T has one or two arguments.
    argument_type
    first_argument_type
    second_argument_type

Function is a template. Like other templates we have used, we must provide additional information when creating a specific function type. In this example, the so-called additional information refers to the call form of the object that the function type can represent. Referring to other templates, we specify the type in a pair of angle brackets

function<int(int,int)>

Here we declare a function type, which can represent a callable object that accepts two ints and returns one int. Therefore, we can use this newly declared type to represent the type used by any desktop calculator

function<int(int,int)> f1=add;
function<int(int,int)> f2=divide()
function<int(int,int)> f3=[](int i,int j){return i%j};

For this function type, we can define map

map<string,functional<int(int,int)>> binops;

We can add all callable objects, including function pointers, lambda or function objects, to this map

map<string,functional<int(int,int)>> binops={
{'+',add},
{'-',std::minus<int>()},
{'/',divide()},
{'*',[](int i,int j){return i*j}},
{'%',mod}
}

The function type overloads the calling operator, which takes its own arguments and passes them to the stored calling object

binops["+"](10,5);
binops["-"](10,5);
binops["/"](10,5);
binops["*"](10,5);
binops["%"](10,5);

Topics: C++