简体   繁体   中英

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?

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

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) ie 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& ie 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) ie 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 ie 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

PS 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” ...

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM