Wednesday, September 27, 2023

function objects and utilities

Overview
Function Objects or Functors are widely used in std library and serve in various modes such as passing ref objects, wrapping up function objects, accessing member functions etc. 

Details

Functors
A functor is an object or structure that can be called like a function by overloading the function call operator () .  Functors also wrap a state during construction which can be used in function calls.
The following shows a functor implementation.
struct Greet
{
    void operator()()
    {
        time_t curr_time;
	curr_time = time(NULL);

        tm *tm_local = localtime(&curr_time);
        size_t h = tm_local->tm_hour;
        cout << "Hello, ";

        if (h > 4 && h < 12)
            cout << "Good Morning";
        else if (h > 11 && h < 17)
            cout << "Good Afternoon";
        else if (h > 16 )
            cout << "Good Evening";
        cout << "!" << endl;
    }        
} g;

//Example
g(); //prints @ 2 am : Hello, Good Evening!
This is depicted in this example.

Lambda Expression
Lambdas are basically anonymous function objects generated right at the location where it's invoked or passed as an argument to a function. Lambdas are game changer since they seamlessly integrate into stl algorithms, concurrency objects and more. 
The syntax for Lambdas is as below.
[capture] (parameters)optional mutableoptional exception-Specoptional -> trailing return Typeoptional
{
    definition of method
Except for the lambda introducer ([]) that contain captured local variables, all of the other  items in the syntax are optional.

Following is a minimum lambda expression, calling itself.
[] //introducer
   // no parameters
{
    time_t curr_time;
    curr_time = time(NULL);

    tm *tm_local = localtime(&curr_time);
    size_t h = tm_local->tm_hour;
    cout << "Hello, ";

    if (h > 4 && h < 12)
        cout << "Good Morning";
    else if (h > 11 && h < 17)
        cout << "Good Afternoon";
    else if (h > 16 )
         cout << "Good Evening";
    cout << "!" << endl;

}   //body
(); //calling itself! 
//prints @ 3 am : Hello, Good Evening!

The following describes it in detail:

  1. capture clause (Also known as the lambda-introducer)
  2. parameters  (Also known as the lambda declarator)
  3. mutable specification.
  4. exception-specification.
  5. trailing-return-type.
  6. lambda definition.
capture
Unlike a normal function, a lambda can also to refer to local variables. It can access them either as read only value or as a reference as described below.
  • [=]  captures all local variables as value readonly
  • [&]  captures all local variables as value readwrite
  • [i,j,&k] captures local variables i and j as value readonly and k as value readwrite
  • [=,&i] captures all local variables as value readonly except i
  • [&,=i]  captures all local variables as value readwrite except i
  • [this] captures all local variables as value readonly
  • [v...] captures variadic variables
If the captured variables are modified, value captured variables do not retain modified value and revert back to original after the call; reference captured variables retain modified values after the call. 
Global variables and static variables need not be captured. They can be accessed as is.
This is demonstrated in the example 2

parameters
Parameters for a lambda are similar to those of normal functions, 
This is depicted in the example 3.

mutable 
This keyword should be used if value captured variables are modified in the implementation. 
This is demonstrated in the example 2

exception specification
noexcept exception specification can be used  to indicate that the lambda expression doesn't throw any exceptions. As with ordinary functions, the compiler generates warning if a lambda expression declares the noexcept exception specification and the lambda body throws an exception.
    //compiler warnings
    []() noexcept { throw 5; }();

trailing return type
The return type for a lambda is specified using a C++ feature named trailing return type. This specification is optional. Without the trailing return type, the return type of the underlying function is effectively auto, and it is deduced from the type of the expressions in the body's return statements.
This is depicted in the example 4.

Lambda in action
Following are some examples where lambdas can be used in stl and concurrency.
This is depicted in the example 5.

The following example demonstrates usage of recursive lambda expression. note auto cannot be used to store lambda expression.
function<void(int)> fib = [t1=0, t2 = 1,t=0]  (int n) mutable
{
    if (n>0)
    {
        cout << t1 << " ";
        t = t2;
        t2 += t1;
        t1 = t;
        fib(n-1);
    }
    else
        cout << " = " << t1 ; 
};
fib(5); //0 1 1 2 3 = 7
fib(9); //0 1 1 2 3 5 8 13 21 = 54  

predefined function objects
Following predefined function objects are defined in the std. library to perform bitwise, relational, logical and arithmetic operations. 

NameDescriptionExample
bit_andBitwise ANDbit_and{}(0xff,0x0c); //returns 0x0c
bit_orBitwise ORbit_or{}(0xf0,0x0c); //returns 0xfc
bit_xorBitwise XORbit_xor{}(0xff,0x0c); //returns 0xf3
not_equal_tonon-equality comparisonnot_equal_to{}(10,2); //returns false
equal_toquality comparisonequal_to{}(10,2); //returns false
greatergreater-than inequality comparisongreater{}(10,2); //returns true
greater_equalgreater-than-or-equal-to comparisongreater_equal{}(10,2); //returns true
lessless-than inequality comparisonless{}(10,2); //returns false
less_equalless-than-or-equal-to comparisonless_equal{}(10,2); //returns false
logical_andLogical ANDlogical_and{}(true,false); //returns false
logical_notLogical NOTlogical_not{}(true); //returns false
logical_orLogical ORlogical_or{}(true,false); //returns true
minusSubtractionminus{}(10,2); //returns 8
modulusModulusmodulus{}(10,2); //returns 0
multipliesMultiplicationmultiplies{}(10,2); //returns 20
negateNegativenegate{}(10); //returns -10
dividesDivisiondivides{}(10,2); //returns 5
plusAdditionplus{}(10,2); //returns 12

This is depicted in this example 6. :
**** bitwise ****
bit_and{}(0xff,0x0c)		= 0xc
bit_or{}(0xf0,0x0c)		= 0xfc
bit_xor{}(0xff,0x0c)		= 0xf3

**** relational ****
not_equal_to{}(10,2)		= true
equal_to{}(10,2)		= false
greater{}(10,2)			= true
greater_equal{}(10,2)		= true
less{}(10,2)			= false
less_equal{}(10,2)		= false

**** logical ****
logical_and{}(true,false)	= false
logical_not{}(true)		= false
logical_or{}(true,false)	= true

**** arithmatic ****
minus{}(10,2)			= 8
modulus{}(10,2)			= 0
multiplies{}(10,2)		= 20
negate{}(10)			= -10
divides{}(10,2)			= 5
plus{}(10,2)			= 12
function pointers
function pointers are pointers to functions that can be defined as variables. function pointer can be defined for "C" type free functions as shown below.
void print(int) {}

    void (*fp)(int) = &print;
    fp(8); //calls print(8);

function pointers can be also defined for members of a class such as fields and functions. However it's not possible to directly assign from an instance of a class. Therefore operators ->* and .* are used exclusively to call functions or access fields of a class as shown below. Notice that function call requires extra parenthesis.
struct Msg
{
    char buf[50];
    void print(){cout << buf;};
} m{};


    char  (Msg::*buf)[50]    = &Msg::buf;
    void (Msg::*fp)()   = &Msg::print;
    strcpy(m.*buf,"hello, world"); //same as m.buf or (&m)->buf
    (m.*fp)(); // same as m.print() or ((&m)->*fp)()

wrapper classes
As seen from the above, function pointers can be tedious to use. Wrapper classes provide alternative. These are classes that hold an object and have an interface similar to that object, but adding or changing some of its features:

function
Class that can wrap any kind of callable element (such as functions and function objects) into a copyable object, and whose type depends solely on its call signature (and not on the callable element type itself).
The syntax is as shown below:
template< class >
class function; /* undefined */

template< class R, class... Args >
class function<R(Args...)>;

An object of a function class instantiation can wrap any of the following kinds of callable objects: a function, a function pointer, a pointer to member, or any kind of function object (i.e., an object whose class defines operator(), including closures).

A decay copy of the wrapped callable object is stored internally by the object, which becomes the function's target member. The specific type of this target callable object is not needed in order to instantiate the function wrapper class; only its call signature.

The example 7 following lists the usage:
***** free function demo *****       
        int square_num(int i)
        {
            return i*i;
        }
    
function<int(int)> {square_num}(-9)				 = 81
function<int(int)> {bind(square_num,placeholders::_1)}(9)	 = 81
function<int()>    {bind(square_num,9)}()			 = 81


***** struct demo *****       
        struct Foo
        {
            Foo(int num) : num_(num) {}
            void print_add(int i) const { cout << num_ + i << endl; }
            int num_;
        } foo {200};
    
function<void(const Foo&, int)>{&Foo::print_add}(foo, 100)			 = 300
function<int(Foo const&)> {&Foo::num_}(foo)					 = 200
function<void(int)> {bind(&Foo::print_add, foo, placeholders::_1)}(2)		 = 202
function<void(int)> {bind(&Foo::print_add, &foo, placeholders::_1)}(3)		 = 203


***** functor demo *****
function<int(int)>{negate<int>()}(18)		 = -18


***** lambda expression demo *****
function<void(int)> {[](int i) { cout << i << endl; }}(42)		 = 42
       
        auto factorial = [](int n)
        {
            // store a lambda object to emulate "recursive lambda"; aware of extra overhead
            function<int(int)> fac = [&](int n) 
            { 
                cout << n << ' '; 
                return (n < 2) ? 1 : fac(n - 1); 
            };
            // note that "auto fac = [&](int n) {...};" does not work in recursive calls
            fac(n);
        };
    
factorial(5)!	 = 5 4 3 2 1 
reference_wrapper
reference_wrapper is a class template that creates a wrapper around a reference to object or reference to function of type T. Instances of reference_wrapper are objects but they are implicitly convertible to T&, so that they can be used as arguments with the functions that take the underlying type by reference.
The syntax is as shown below:
template <class T> class reference_wrapper;

The example 8 following lists the usage:
int a(10),b(20),c(30);

// array of int&
reference_wrapper<int> refs[] = {a,b,c};
// vector of int&
vector<reference_wrapper<int>> vec {a,b,c};

placeholder namespace
This namespace defines an unspecified number of objects: _1, _2, _3,..., which are used to specify placeholders in calls to function bind. When the function object returned by bind is called, an argument with placeholder::_1 is replaced by the first argument in the call, placeholder::_2 is replaced by the second argument in the call, and so on.
is_placeholder trait class can be used to check if the argument is a placeholder type or not.

functions
These functions are defined to help with reference and function wrapper classes discussed above.

ref and cref
Function templates ref and cref are helper functions that generate an object of type reference_wrapper, using template argument deduction to determine the template argument of the result.

The example 9 following lists the usage:
void f(int& n1, int& n2, const int& n3) {}
 
// Example
int n1 = 1, n2 = 2, n3 = 3;
function<void()> bound_f = std::bind(f, n1, std::ref(n2), std::cref(n3));
n1 = 10; n2 = 11; n3 = 12;
bound_f(); //calls f(1, 11, 12)

mem_fn
This function converts member function to a functor whose functional call invokes the member function pointed by pm.
The syntax is as shown below:
template <class Ret, class T> 
 /* unspecified */ mem_fn (Ret T::* pm);
ts functional call takes as first argument an object of type T (or a reference or a pointer to it) and, as additional arguments, the arguments taken by pm (if any). The effect of such a call with fn as first argument are the same as calling fn.*pm (or (*fn).*pm if fn is a pointer), forwarding any additional arguments.

The example 10 following lists the usage:
struct Foo
        {
            void display_greeting();
            void display_number(int i);
            int add_xy(int x, int y);
            template<typename... Args> int add_many(Args... args);
            int data = 7;
        } f;
        auto u = make_unique<Foo>();
    

**** using instance ****
display_greeting(f)		 = Hello, world.
display_number(f,42)		 = 42
data(f)				 = 7
add_xy(f, 1, 2)			 = 10

**** using unique_ptr ****
data(u)				 = 7
add_xy(u, 1, 2)			 = 10
add_many(u, 1, 2, 3)		 = 13
bind
Returns a function object based on fn, but with its arguments bound to args. The syntax is as shown below.
template <class Fn, class... Args>  
/* unspecified */ bind (Fn&& fn, Args&&... args);
template <class Ret, class Fn, class... Args>  
/* unspecified */ bind (Fn&& fn, Args&&... args);
  • fn is a function object, pointer to function or pointer to member.
  • args is a list of arguments. Each argument may either be bound to a value or be a placeholder. If bound to a value, calling the returned function object will always use that value as argument otherwise the value passed in the call will be forwarded at runtime. 
  • ret A function object that, when called, calls fn with its arguments bound to args.
The example 11 following lists the usage:
 struct MyPair 
        {
            double a,b;
            double multiply() {return a*b;}
        } ten_two {10,2};
    
bind (divides<int>{},10,2)()						 = 5
bind (divides<int>{},placeholders::_1,2)(10)				 = 5
bind (divides<int>{},placeholders::_2,placeholders::_1)(10,2)		 = 0
bind<int> (divides<int>{},placeholders::_1,placeholders::_2)(10,3)	 = 3
bind (&MyPair::multiply,placeholders::_1)(ten_two)			 = 20
bind (&MyPair::a,ten_two)()						 = 10
The example 12 following lists the usage:
    void f(int n1, int n2, int n3, const int& n4, int n5);
        int g(int n1);
 
        struct Foo
        {
            void print_sum(int n1, int n2);
        int data = 10;
        } foo;
    
1) argument reordering and pass-by-reference: 
 bind(f, placeholders::_2, 42, placeholders::_1, cref(n), n)(1, 2, 1001) = 2 42 1 10 7

2) achieving the same effect using a lambda: 
[&ncref = n, n](auto a, auto b, auto /*unused*/)(1, 2, 1001) = 2 42 1 10 7

3) nested bind subexpressions share the placeholders: 
bind(f, placeholders::_3, bind(g, placeholders::_3), placeholders::_3, 4, 5)(10, 11, 12) = 12 12 12 4 5

4) bind a RNG with a distribution: 
bind(uniform_int_distribution<>(0, 10), default_random_engine{})() = 1
5) bind to a pointer to member function: 
bind(&Foo::print_sum, &foo, 95, placeholders::_1) = 100

6) bind to a mem_fn that is a pointer to member function: 
bind(mem_fn(&Foo::print_sum), &foo, 95, placeholders::_1) = 100

7) bind to a pointer to data member: 
bind(&Foo::data, placeholders::_1)(foo) = 10

8) bind to a mem_fn that is a pointer to data member: 
bind(mem_fn(&Foo::data), placeholders::_1)(foo) = 10

9) use smart pointers to call members of the referenced objects: 
bind(mem_fn(&Foo::data), placeholders::_1)(make_shared<Foo>(foo)) = 10
bind(mem_fn(&Foo::data), placeholders::_1)(make_unique<Foo>(foo)) = 10
Summary of Examples
NameDescriptiongithubwandbox
  Example       Generic Functor  source    output    source + output 
  Example 2       Lambda - Capture and mutable  source    output    source + output 
  Example 3Lambda - Parameters  source    output  source + output 
  Example 4Lambda - Trailing return type  source    output  source + output  
  Example 5Lambda - in Action  source    output  source + output  
  Example 6Predefined functors  source    output  source + output  
  Example 7function  source    output  source + output  
  Example 8reference_wrapper  source    output  source + output  
  Example 9ref and cref  source    output  source + output  
  Example 10mem_fn  source    output  source + output  
  Example 11bind - simple  source    output  source + output  
  Example 12bind - complex  source    output  source + output  




No comments:

Post a Comment