Deep Exploration of Function Pointer

Posted by studot on Sun, 09 Jan 2022 18:35:52 +0100

Re-exploration of Function Pointer

Questions raised

  • What is the nature of function pointers?
  • Is a function pointer as efficient as a direct call? Why?
  • Can function pointers be combined with templates?
  • Can function pointers work on static functions, member methods, static methods, pure virtual functions, lamda? What can I do if I can't?
  • What other code besides function pointers can do similar things?

explore

What is the nature of function pointers?

This refers to the definition of function pointers recorded in Wikipedia

A function pointer, also called a subroutine pointer or procedure pointer is a pointer that point to a function,

A function is a block of code that performs a specific function and an executable interval. At the assembly language level, a calling function generally saves some current information, such as program counters and other information, onto the stack, then jumps to the entry address where the function executes, returns from the body of the function, and returns to the current main program.

In one sentence:

The essence of a function pointer is to point to the first address of an executable block of code, and a function is the first address of that executable block of code

A function can be called directly, while a function pointer needs to be dereferenced when it calls a function, but they both implement the same functions, one directly and the other indirectly.

Compare the two types of function calls at the assembly level, such as a function add, which is called in both ways mentioned earlier. Disassemble and view the underlying call format as follows

int add(int a, int b){
    return a + b;
}

typedef decltype(add) *FP;

int main(){

    // Calling functions directly
    int c1 = add(1,2);

    // Calling a function through a function pointer
    FP p = add;
    int c2= p(1,2);

    return 0;
}

The disassembly assembly code section is as follows, deleting the details and looking at only the parts called by the two functions. The disassembly code is as follows

.....
# The first address of the add function
0000000140001530 <_Z3addii>:
   140001530:   55                      push   %rbp
   140001531:   48 89 e5                mov    %rsp,%rbp
   140001534:   89 4d 10                mov    %ecx,0x10(%rbp)
   140001537:   89 55 18                mov    %edx,0x18(%rbp)
   14000153a:   8b 55 10                mov    0x10(%rbp),%edx
   14000153d:   8b 45 18                mov    0x18(%rbp),%eax
   140001540:   01 d0                   add    %edx,%eax
   140001542:   5d                      pop    %rbp
   140001543:   c3                      retq

# First address of main function
0000000140001544 <main>:
	......
   140001551:   ba 02 00 00 00          mov    $0x2,%edx
   140001556:   b9 01 00 00 00          mov    $0x1,%ecx
   14000155b:   e8 d0 ff ff ff          callq  140001530 <_Z3addii>
   
   
   140001560:   89 45 fc                mov    %eax,-0x4(%rbp)
   140001563:   48 8d 05 c6 ff ff ff    lea    -0x3a(%rip),%rax        # 140001530 <_Z3addii>
   14000156a:   48 89 45 f0             mov    %rax,-0x10(%rbp)
   14000156e:   48 8b 45 f0             mov    -0x10(%rbp),%rax
   140001572:   ba 02 00 00 00          mov    $0x2,%edx
   140001577:   b9 01 00 00 00          mov    $0x1,%ecx
   14000157c:   ff d0                   callq  *%rax
.....

From the analysis, you can see two assembly codes. The assembly code used by the function call is

callq 140001530

The assembly code for the function pointer calling the function is

lea -0x3a(%rip), %rax  ;take add The first address of the function is put in rax register
callq *%rax			   ;Indirect Call add,add Address stored in rax inside

As you can see, a function pointer call simply places the function's first address in a register and indirectly jumps to that location

How effective is a function pointer compared to a function call?

Find a description of the performance of function pointer calls from Wiki pedia as

Extensively using function pointers to call functions may produce a slow-down for the code on modern processors, because branch predictor may not be able to figure out where to branch to (it depends on the value of the function pointer at run time) although this effect can be overstated as it is often amply compensated for by significantly reduced non-indexed table lookups.

On modern processors, heavy use of function pointers to call functions can lead to slower code because branch predictors may not be able to determine where to branch (depending on the value of the runtime function pointer), although this effect may be exaggerated because it is often compensated for by a large reduction in non-index table lookups

Come from Function pointer - Wikipedia

This may mean that the performance differences between the two are not significant

Can functions be combined with templates?

Can be combined with templates

#include<stdio.h>

template<typename type>
type add(type type1, type type2){ return type1 + type2;}

void func1(double (*p1)(double, double), double a, double b){
    printf("The double value %f + %f = %f\n", a, b, p1(a,b));
}

void func2(int (*p1)(int, int), int a, int b){
    printf("The integer value %d + %d = %d\n", a, b, p1(a,b));
}

int main(int argc, char *argv[]){
    func1(add<double>, 1.0, 2.0);
    func2(add<int>, 1, 2);
    return 0;
}
D:\ClionProject\C++Learn\cmake-build-debug\C__Learn.exe
The double value 1.000000 + 2.000000 = 3.000000
The integer value 1 + 2 = 3

What defined function templates can a function pointer point to

Can function pointers work on static functions, member methods, static methods, pure virtual functions, lamda, or what can I do instead?

Acting on static functions

#include<stdio.h>

// Static Function Definition
static int add_func(int a, int b){
    return a + b;
}

int main(int argc, char *argv[]){

    typedef int (*calcu_ptr)(int, int);  // Define function pointer type

    printf("The func pointer's size is %d\n", int(sizeof(calcu_ptr)));
    
    calcu_ptr c = &add_func;  // Function pointer to add_func

    printf("The result of the %d + %d = %d", 1, 2, c(1,2));

    return 0;
}
D:\ClionProject\C++Learn\cmake-build-debug\C__Learn.exe
The func pointer's size is 8
The result of the 1 + 2 = 3

Act on member methods

#include<stdio.h>

class ALU{
    // Complete some basic operations
public:
    int alu_Add(int a, int b){
        return a + b;
    }
};

int main(int argc, char *argv[]){
    
    typedef int (ALU::*calc_ptr)(int, int); // Define pointers to member methods
    
    calc_ptr c = &ALU::alu_Add;			    // Common member methods pointing to classes
    
    printf("The member func pointer's size is %d\n", int(sizeof (calc_ptr)));
    
    ALU *a = new ALU();					
    
    printf("The result of %d + %d = %d\n", 1, 2,(a->*c)(1,2));  // Use of the normal member method
    
    return 0;
}
D:\ClionProject\C++Learn\cmake-build-debug\C__Learn.exe
The member func pointer's size is 16
The result of 1 + 2 = 3

Point to Member Static Method

#include<stdio.h>

class ALU{
    // Complete some basic operations
public:
    static int alu_Add(int a, int b){
        return a + b;
    }
};

int main(int argc, char *argv[]){

    typedef int (*calc_ptr)(int, int); // Define Function Pointer

    calc_ptr c = &ALU::alu_Add;		   // Point to static member method   

    printf("The static member func pointer's size is %d\n", int(sizeof (calc_ptr)));

    printf("The result of %d + %d = %d\n", 1, 2,c(1,2));  // Use static member method

    return 0;
}
D:\ClionProject\C++Learn\cmake-build-debug\C__Learn.exe
The static member func pointer's size is 8
The result of 1 + 2 = 3

Point to Pure Virtual Function

#include<stdio.h>

class Operation{
public:
    virtual int Calcu(int a, int b) = 0;
};

class Add:public Operation{
public:
    int Calcu(int a, int b) override{ return a + b; }
};

class Sub:public Operation{
public:
    int Calcu(int a, int b) override{ return a - b; }
};

int main(int argc, char *argv[]){

    typedef int (Operation::*Calcu)(int, int);

    Calcu c = &Operation::Calcu;

    printf("The virtual member func's size is %d\n", int(sizeof(Calcu)));

    Add a;
    Sub s;
    
    printf("%d + %d = %d\n", 1, 2, (a.*c)(1,2));
    printf("%d - %d = %d\n", 2, 1, (s.*c)(2,1));

    return 0;
}
D:\ClionProject\C++Learn\cmake-build-debug\C__Learn.exe
The virtual member func's size is 16
1 + 2 = 3
2 - 1 = 1

Function pointer to lamda

#include<stdio.h>

typedef int (*Calcu)(int, int);

int main(){
    Calcu c = [](int a, int b){return a + b;};

    printf("The func pointer size is %d\n", int(sizeof (Calcu)));

    printf("%d + %d = %d\n", 1, 2, c(1,2));

    return 0;
}
D:\ClionProject\C++Learn\cmake-build-debug\C__Learn.exe
The func pointer size is 8
1 + 2 = 3

From the code above, you can draw the following conclusions

  • Function pointers can act on functions, member methods, static member methods, pure virtual functions, lamda
  • When a function pointer points to a member method and a pure virtual function, it is twice as large as a normal function pointer, and its definition method is different, twice as large as a normal function pointer, because these methods are executed with a specific object, and the function pointer points not only to the address of the method, but also to the address of the owner (object) of the method. So it's two to three times larger than a normal function pointer

In addition to function pointers, what else can you do to achieve similar functionality

functor, a function is simply a statement block. It can implement some functions and return values. This is a function, but in the process of function call, the function executed by the function can only be handled by passing parameters and calling some global variables. The key is that it has no state, and the function object will not be like this. It defines a function object as not only a function but also a state of its own. If a piece of code can implement such a function, it is a function object. In C++, one way to implement a function object is to overload the () operator.

For example, an auto-cola vending machine, which only has 10 cans of cola, the user obtains the cola through the function buyCola. When 10 cans are bought, there will be no cola anymore, so you need to give a hint of selling out. In such a scenario, function alone cannot be achieved, but it can be achieved through the ** function object (or function functor)**, Because the latter can record this state, see the code

#include<stdio.h>

class BuyCola{

public:
    BuyCola(int cola_count):cola_count(cola_count){}
	// overload operators
    void operator()(){
        if(cola_count > 0){
            printf("You will buy a cola\n");
            cola_count--;
        }else{
            printf("The cola is sold out!\n");
        }
    }

private:
    // Number of Cokes
    int cola_count;  
};

int main(){
    BuyCola vending_maching(10);
    for (int i = 0; i < 13; ++i){
        vending_maching();
    }
    return 0;
}
D:\ClionProject\C++Learn\cmake-build-debug\C__Learn.exe
You will buy a cola
You will buy a cola
You will buy a cola
You will buy a cola
You will buy a cola
You will buy a cola
You will buy a cola
You will buy a cola
You will buy a cola
You will buy a cola
The cola is sold out!
The cola is sold out!
The cola is sold out!

For the introduction of specific functions, you can refer to Wiki or other information. The above is only personal understanding. If there are any errors, you are welcome to point out!

Here is the original Wiki intercept

A typical use of a function object is in writing callback functions. A callback in procedural languages, such as C, may be performed by using function pointers.[2] However it can be difficult or awkward to pass a state into or out of the callback function. This restriction also inhibits more dynamic behavior of the function. A function object solves those problems since the function is really a façade for a full object, carrying its own state.

Many modern (and some older) languages, e.g. C++, Eiffel, Groovy, Lisp, Smalltalk, Perl, PHP, Python, Ruby, Scala, and many others, support first-class function objects and may even make significant use of them.[3] Functional programming languages additionally support closures, i.e. first-class functions that can 'close over' variables in their surrounding environment at creation time. During compilation, a transformation known as lambda lifting converts the closures into function objects.

Function object - Wikipedia

Topics: C++