又是 C++ 函数指针。语法混乱

C++ function pointers, again. Confusion regarding syntax

this page 上,我发现了一个很好的 C++ 函数指针示例(以及仿函数,但这个问题与仿函数无关)。下面是该页面的一些副本。

#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;
}

我大体上理解代码,但有几件事我总是感到困惑。函数binary_op()接受了一个函数指针*f,但是在使用的时候,比如第19行binary_op(a, b, add),传入的是函数符号add,不是大家想的那样作为它的指针,&add。现在你可能会说这是因为符号 add 一个指针;是函数add()对应的那段代码的地址。很好,但是这里似乎仍然存在类型差异。函数 binary_op() 采用 *f,这意味着 f 是指向某物的指针。我传入 add,它本身就是一个指向代码的指针。 (对吗?)所以 f 被赋值为 add,这使得 f 成为指向代码的指针,这意味着 f 是一个函数,就像 [=15] =],这意味着 f 应该像 f(left, right) 一样调用,确切地说 add 应该如何调用,但是在第 12 行,它被称为 (*f)(left, right),这并不对我来说似乎是正确的,因为它就像写 (*add)(left, right),而 *add 不是函数,它是 add 指向的代码的第一个字符。 (对吗?)

我知道将 binary_op() 的原始定义替换为以下内容也有效。

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

事实上,这对我来说更有意义,但正如我上面解释的那样,原始语法没有意义。

那么,为什么使用 (*f) 而不是 f 在语法上是正确的?如果符号 func 本身就是一个指针,那么短语 "function pointer" 或 "pointer to a function" 究竟意味着什么?按照目前的原码,写double (*f)(double, double),那么f是什么东西呢?指向指针的指针(因为 (*f) 本身就是指向一段代码的指针)?符号 add 是和 (*f) 一样的东西,还是和 f 一样的东西?

现在,如果所有这一切的答案是"Yeah C++ syntax is weird, just memorise function pointer syntax and don't question it.",那我勉强接受,但我真的很想对我的事情有一个适当的解释我想错了。

我读过 and I think I understand that, but haven't found it helpful in addressing my confusion. I've also read this question,这也没有帮助,因为它没有直接解决我的类型差异问题。我可以继续阅读互联网上的海量信息来找到我的答案,但是嘿,这就是 Stack Overflow 的用武之地吗?

这是因为C函数指针比较特殊

首先,表达式 add 会衰减为一个指针。就像对数组的引用会衰减为指针一样,对函数的引用也会衰减为指向函数的指针。

然后,那里有奇怪的东西:

return (*f)(left, right);

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

两者都有效,你可以这样重写代码:

return f(left, right);

这是因为取消引用运算符将​​ return 对函数的引用,并且对函数的引用或函数指针都被认为是可调用的。

有趣的是,函数引用很容易衰减,以至于在调用取消引用运算符时它会退化回指针,从而可以根据需要多次取消引用函数:

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?

f 的类型是 double (*)(double, double) 即它是一个指向类型 double(double,double) 的函数的指针。

because (*f) is itself a pointer

不是。

问:通过指针间接访问会得到什么(例如 *f)? A:你得到一个左值引用。例如,给定一个对象指针 int* ptr,表达式 *ptr 的类型是 int&int.

的左值引用

函数指针也是如此:当您间接通过函数指针时,您将获得指向函数的左值引用。在 *f 的情况下,类型是 double (&)(double, double) 即引用类型 double(double,double).

的函数

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

非限定 id 表达式 add*f 是同一类东西,即它是一个左值:

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;

没有。不是这个原因。

add 不是指针。它是一个左值。但是函数类型的左值隐式转换为指针(这称为衰减):

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.

因此,以下内容在语义上是等价的:

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?

事实证明,调用函数左值与调用函数指针具有相同的语法:

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.

这些都是相同的函数调用:

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

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

P.S。这两个声明是等价的:

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

这是因为以下规则,它是对函数指针隐式衰减的补充:

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

首先,当编译器确定参数的类型时,将指定为函数声明的函数参数调整为指向函数的指针。因此,例如以下函数声明

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

是等价的并且声明了相同的函数。

考虑以下演示程序

#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 );
}

来自 c++ 17 标准(11.3.5 函数):

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

另一方面,根据 C++ 17 标准

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

那么这两个声明有什么区别

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

对于第一个声明,您可以将函数体内的参数 h 视为函数指针的类型定义。

typedef void ( *H )();

例如

#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 );
}

根据 C++ 17 标准(8.5.1.2 函数调用)

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.

所以你也可以这样定义函数

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

甚至喜欢

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

因为当运算符 * 应用于函数名称时,函数名称会隐式转换为函数的指针。