At their core, functions are nothing more than a location in memory with some special semantics attached to them. The assumption is that the location in memory is the start of some machine code to be executed, and that there are conventions to follow when jumping to that address. But at the end of the day, functions are just locations in memory. As such, they can be viewed in one of two ways: as an entity which executes instructions to manipulate some program state, or as data. I want to discuss the times when we view functions as data.
When talking about functions as data, what we’re really talking about is a function pointer. This is a way for us to refer to functions purely as data objects. For instance, we can store them in a list, pass them as parameters, etc. As with all pieces of data in C and C++, there is a type to be discussed. The type of a function pointer is the signature of the function. For instance, these two declarations are mostly equivalent:
int Foobar1( int i, double d ); int (*Foobar2)( int i, double d );
I say “mostly” because the types are distinct, but only barely. Foobar1 is declared as being a function. That means you cannot assign anything to Foobar1, it is an rvalue. Foobar2 is declared as a function pointer, so you can use it as a type to hold a reference to a function. For instance:
Foobar2 = Foobar1; // Foobar2 now points to the memory location of Foobar1
Now Foobar2 and Foobar1 both point to the same location in memory — using either a function by calling it will jump to the same memory location. This is an incredibly powerful concept because it means you are able to refer to functions as pieces of data. Taking a more concrete example, let’s say I have a list of functions I want to call in succession:
int Foobar3( int, double ); // Defined elsewhere std::vector< int (*)( int, double ) > funcList; funcList.push_back( Foobar1 ); funcList.push_back( Foobar3 ); int sum = 0; for (auto f : funcList ) { sum += f( 10, 3.14 ); }
The for loop will call every function that we’ve pushed into the function list.
While function pointers give you a phenomenal amount of power as a programmer, they also come with a sizable amount of danger. Because function pointers have a type associated with them, it’s possible for you to use typecasts to trick the compiler into doing bad things. For instance:
int Foobar1( double d, int i ); int (*Foobar2)(int i, double d ); Foobar2 = (int (*)(int, double))Foobar1; int temp = Foobar2( 10, 3.14 ); ::printf( "%d\n", temp );
You’ll notice that Foobar1’s method signature has the double as the first parameter, and the integer as the second parameter. Yet we’re reversing the order of the parameters and forcing the compiler to accept it. This code will compile, but it won’t do anything good. If we look at the disassembly for this, the problem will become apparent:
; Allocate space for the double 000C8AC8 mov esi,esp 000C8ACA sub esp,8 ; Store the value 3.14 on the FPU stack 000C8ACD fld qword ptr [__real@40091eb851eb851f (0E3DC0h)] 000C8AD3 fstp qword ptr [esp] ; Store the value 10 on the stack 000C8AD6 push 0Ah ; Call Foobar2 000C8AD8 call dword ptr [Foobar2 (0EF210h)] ; Get the return value into our temp variable 000C8AE8 mov dword ptr [temp],eax
The problem is that it is storing the double first, then the integer. Yet when Foobar1 (the actual function pointer) is called, it expects to get the integer first, and the double second. So the integer will hold a truncated portion of the encoded double value, and the double will hold a randomly-padded portion of the integer value. Neither parameter will be close to correct.
So what can you do to avoid this? Unfortunately, the answer is “nothing.” Because you can typecast, you can always trick the compiler into doing whatever you want it to do. The best suggestion is: if the compiler gives you an error when working with function pointers, assume you are doing something wrong. Typecasting a function pointer is never the answer, unless the question is “what should I never do?”