Ray
Ray

Reputation: 8573

C++ function pointers, again. Confusion regarding syntax

On this page I found a good example of function pointers in C++ (as well as of functors, but this question isn't about functors). Below is some copypasta from that page.

#include <iostream>

double add(double left, double right) {
  return left + right;
}

double multiply(double left, double right) {
  return left * right;
}

double binary_op(double left, double right, double (*f)(double, double)) {
  return (*f)(left, right);
}

int main( ) {
  double a = 5.0;
  double b = 10.0;

  std::cout << "Add: " << binary_op(a, b, add) << std::endl;
  std::cout << "Multiply: " << binary_op(a, b, multiply) << std::endl;

  return 0;
}

I understand the code in general terms, but there are a couple of things that I've always found confusing. Function binary_op() takes a function pointer *f, but when it's used, for example on line 19 binary_op(a, b, add), the function symbol add is passed in, not what one would think of as its pointer, &add. Now you may say that this is because the symbol add is a pointer; it's the address of the bit of code corresponding to the function add(). Very well, but then there still seems to be a type discrepancy here. The function binary_op() takes *f, which means f is a pointer to something. I pass in add, which itself is a pointer to code. (Right?) So then f is assigned the value of add, which makes f a pointer to code, which means that f is a function just like add, which means that f should be called like f(left, right), exactly how add should be called, but on line 12, it's called like (*f)(left, right), which doesn't seem right to me because it would be like writing (*add)(left, right), and *add isn't the function, it's the first character of the code that add points to. (Right?)

I know that replacing the original definition of binary_op() with the following also works.

double binary_op(double left, double right, double f(double, double)) {
  return f(left, right);
}

And in fact, this makes much more sense to me, but the original syntax doesn't make sense as I explained above.

So, why is it syntactically correct to use (*f) instead of just f? If the symbol func is itself a pointer, then what precisely does the phrase "function pointer" or "pointer to a function" mean? As the original code currently stands, when we write double (*f)(double, double), what kind of thing is f then? A pointer to a pointer (because (*f) is itself a pointer to a bit of code)? Is the symbol add the same sort of thing as (*f), or the same sort of thing as f?

Now, if the answer to all of this is "Yeah C++ syntax is weird, just memorise function pointer syntax and don't question it.", then I'll reluctantly accept it, but I would really like a proper explanation of what I'm thinking wrong here.

I've read this question and I think I understand that, but haven't found it helpful in addressing my confusion. I've also read this question, which also didn't help because it doesn't directly address my type discrepancy problem. I could keep reading the sea of information on the internet to find my answer but hey, that's what Stack Overflow is for right?

Upvotes: 3

Views: 321

Answers (3)

eerorika
eerorika

Reputation: 238311

As the original code currently stands, when we write double (*f)(double, double), what kind of thing is f then?

The type of f is double (*)(double, double) i.e. it is a pointer to a function of type double(double,double).

because (*f) is itself a pointer

It is not.

Q: What do you get when you indirect through a pointer (such as in *f)? A: You get an lvalue reference. For example, given an object pointer int* ptr, the type of the expression *ptr is int& i.e. lvalue reference to int.

The same is true for function pointers: When you indirect through a function pointer, you get an lvalue reference to the pointed function. In the case of *f, the type is double (&)(double, double) i.e. reference to function of type double(double,double).

Is the symbol add the same sort of thing as (*f), or the same sort of thing as f?

The unqualified id expression add is the same sort of thing as *f i.e. it is an lvalue:

Standard draft [expr.prim.id.unqual]

... The expression is an lvalue if the entity is a function ...


the function symbol add is passed in, not what one would think of as its pointer, &add. Now you may say that this is because the symbol add is a pointer;

No. That's not the reason.

add is not a pointer. It is an lvalue. But lvalues of function type implicitly convert to a pointer (this is called decaying):

Standard draft [conv.func]

An lvalue of function type T can be converted to a prvalue of type “pointer to T”. The result is a pointer to the function.

As such, the following are semantically equivalent:

binary_op(a, b,  add); // implicit function-to-pointer conversion
binary_op(a, b, &add); // explicit use of addressof operator

So, why is it syntactically correct to use (*f) instead of just f?

Turns out that calling a function lvalue has the same syntax as calling a function pointer:

Standard draft [expr.call]

A function call is a postfix expression followed by parentheses containing a possibly empty, comma-separated list of initializer-clauses which constitute the arguments to the function. The postfix expression shall have function type or function pointer type. For a call to a non-member function or to a static member function, the postfix expression shall either be an lvalue that refers to a function (in which case the function-to-pointer standard conversion ([conv.func]) is suppressed on the postfix expression), or have function pointer type.

These are all the same function call:

add(parameter_list);    // lvalue
(*f)(parameter_list);   // lvalue

(&add)(parameter_list); // pointer
f(parameter_list);      // pointer

P.S. These two declarations are equivalent:

double binary_op(double, double, double (*)(double, double))
double binary_op(double, double, double    (double, double))

This is because of the following rule, which is complementary to the implicit decay into function pointer:

Standard draft [dcl.fct]

The type of a function is determined using the following rules. The type of each parameter (including function parameter packs) is determined from its own decl-specifier-seq and declarator. After determining the type of each parameter, any parameter of type “array of T” or of function type T is adjusted to be “pointer to T” ...

Upvotes: 1

Vlad from Moscow
Vlad from Moscow

Reputation: 310950

First of all a function parameter specified as a function declaration is adjusted to pointer to the function when the compiler determinates the type of the parameter. So for example following function declarations

void f( void h() );
void f( void ( *h )() );

are equivalent and declare the same one function.

Consider the following demonstrative program

#include <iostream>

void f( void h() );
void f( void ( *h )() );

void h() { std::cout << "Hello Ray\n"; }

void f( void h() ) { h(); }

int main()
{
    f( h );
}

From the c++ 17 Standard (11.3.5 Functions):

5 The type of a function is determined using the following rules. The type of each parameter (including function parameter packs) is determined from its own decl-specifier-seq and declarator. After determining the type of each parameter, any parameter of type “array of T” or of function type T is adjusted to be “pointer to T”.

On the other hand, according to the C++ 17 Standard

9 When there is no parameter for a given argument, the argument is passed in such a way that the receiving function can obtain the value of the argument by invoking va_arg (21.11). [ Note: This paragraph does not apply to arguments passed to a function parameter pack. Function parameter packs are expanded during template instantiation (17.6.3), thus each such argument has a corresponding parameter when a function template specialization is actually called. — end note ] The lvalue-to-rvalue (7.1), array-to-pointer (7.2), and function-to-pointer (7.3) standard conversions are performed on the argument expression

So what is the difference between these two declarations

void f( void h() );
void f( void ( *h )() );

For the first declaration you may consider the parameter h within the function body like a typedef for a function pointer.

typedef void ( *H )();

For example

#include <iostream>

void f( void h() );
void f( void ( *h )() );

void h() { std::cout << "Hello Ray\n"; }


typedef void ( *H )();

void f( H h ) { h(); }

int main()
{
    f( h );
}

According to the C++ 17 Standard (8.5.1.2 Function call)

1 A function call is a postfix expression followed by parentheses containing a possibly empty, comma-separated list of initializer-clauses which constitute the arguments to the function. The postfix expression shall have function type or function pointer type.

So you may also define the function like

void f( void h() ) { ( *h )(); }

Or even like

void f( void h() ) { ( ******h )(); }

because when the operator * is applied to a function name then the function name is implicitly convereted to pijnter to the function.

Upvotes: 1

Guillaume Racicot
Guillaume Racicot

Reputation: 41760

This is because C function pointer are special.

First of, the expression add will decay into a pointer. Just like reference to array will decay into a pointer, reference to function will decay into a pointer to function.

Then, the weird stuff it there:

return (*f)(left, right);

So, why is it syntactically correct to use (*f) instead of just f?

Both are valid, you can rewrite the code like this:

return f(left, right);

This is because the dereference operator will return the reference to the function, and both a reference to a function or a function pointer are considered callable.

The funny thing is that a function reference decay so easily that it will decay back into a pointer when calling the dereference operator, allowing to dereference the function as many time as you want:

return (*******f)(left, right); // ah! still works

Upvotes: 3

Related Questions