C++函数指针,再探讨。关于语法的困惑。

3
这个页面中,我找到了一个关于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就像add一样是一个函数,这意味着f应该像add一样被调用,如f(left, right),但是在第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本身就是一个指针,那么短语“函数指针”或“指向函数的指针”具体是什么意思呢?如原始代码所示,当我们写double (*f)(double, double)时,f到底是什么类型的东西呢?是一个指向指针的指针(因为(*f)本身就是指向一小段代码的指针)吗?符号add(*f)是同一类型的东西,还是与f是同一类型的东西?现在,如果所有问题的答案都是“是的,C++语法很奇怪,只需记住函数指针语法,不要质疑它”,那么我会勉强接受它,但我真的很想得到一个正确的解释。我已经阅读了这个问题,我认为我理解了,但它没有帮助我解决我的困惑。我还阅读了这个问题,但它也没有帮助,因为它没有直接解决我的类型差异问题。我可以继续阅读互联网上的信息来找到答案,但嘿,这就是Stack Overflow的用途,不是吗?

1
如果您想要章节和诗句,您可能需要添加“language-lawyer”标签。这将涉及隐式函数指针转换和函数指针的函数调用。 - Eljay
语言允许将函数名隐式转换为函数指针,并通过函数指针进行调用。因此,两种变体都是正确的。 - Slava
related - anatolyg
3个回答

3

这是因为C函数指针很特殊。

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

然后,奇怪的东西就在这里:

return (*f)(left, right);

那么,为什么在语法上使用(*f)而不只是f是正确的呢?

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

return f(left, right);

这是因为解引用运算符将返回对函数的引用,而对函数的引用或指向函数的指针均被视为可调用对象。
有趣的是,函数引用很容易衰变到通过调用解引用运算符时会衰变回指针,这允许您无限次地对函数进行解引用:
return (*******f)(left, right); // ah! still works

1
原始代码中,当我们编写 double (*f)(double, double) 时,f 是什么类型的东西? f 的类型是 double (*)(double, double),即它是一个指向类型为 double(double,double) 的函数的指针。
因为 (*f) 本身不是一个指针。
问:通过指针间接引用(例如在 *f 中)会得到什么?答:您将得到一个 lvalue 引用。例如,给定一个对象指针 int* ptr,表达式 *ptr 的类型是 int&,即对 int 的 lvalue 引用。
对于函数指针也是如此:当您通过函数指针间接引用时,您将得到指向所指函数的 lvalue 引用。在 *f 的情况下,类型是 double (&)(double, double),即类型为 double(double,double) 的函数的引用。

符号 add(*f) 是同一种类型的东西,还是和 f 是同一种类型的东西?

不带限定词的标识表达式 add*f 是同一种类型的东西,也就是说它是一个左值:

标准草案 [expr.prim.id.unqual]

... 如果实体是一个函数,表达式就是一个左值 ...


传递的是函数符号 add,而不是它的指针 &add。你可能会认为这是因为符号 add 是一个指针;但事实并非如此。add 不是一个指针,它是一个左值。但函数类型的左值会隐式转换为指针(称为衰减):
标准草案 [conv.func]
函数类型 T 的左值可以转换为类型为 "T 指针" 的纯右值。结果是指向函数的指针。
因此,以下语句在语义上等效:
binary_op(a, b,  add); // implicit function-to-pointer conversion
binary_op(a, b, &add); // explicit use of addressof operator

因此,为什么使用(*f)而不是f在语法上是正确的呢?原来,调用函数lvalue与调用函数指针具有相同的语法:

标准草案 [expr.call]

函数调用是后缀表达式,后面跟着括号,其中包含可能为空的逗号分隔的初始化器列表,它们构成函数的参数。后缀表达式应具有函数类型或函数指针类型。对于调用非成员函数或静态成员函数,后缀表达式应该是引用函数的左值(在这种情况下,函数到指针的标准转换[conv.func]被抑制),或者具有函数指针类型。

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

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

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

附注:这两个声明是等效的:

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

这是因为以下规则与隐式退化成函数指针相补充:
标准草案 [dcl.fct]
使用以下规则确定函数的类型。每个参数的类型(包括函数参数包)都是从它自己的decl-specifier-seq和declarator中确定的。确定每个参数的类型后,任何类型为“T数组”或“类型为T的函数”的参数都会被调整为“指向T的指针”。

1

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

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 函数的类型由以下规则确定。每个参数的类型(包括函数参数包)都是从其自己的 decl-specifier-seq 和 declarator 确定的。在确定每个参数的类型之后,“T 类型的数组”或“T 类型的函数”中的任何参数都会调整为“指向 T 的指针”。

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

9 当没有给定参数时,以这种方式传递参数,以便接收函数可以通过调用 va_arg(21.11) 获得参数的值。[注意:此段不适用于传递给函数参数包的参数。函数参数包在模板实例化期间扩展(17.6.3),因此当实际调用函数模板特化时,每个这样的参数都有相应的参数。—end note] 在参数表达式上执行左值到右值转换(7.1)、数组到指针转换(7.2)和函数到指针转换(7.3)

那么这两个声明有什么区别呢?
void f( void h() );
void f( void ( *h )() );

对于第一个声明,您可以将函数体内的参数h视为函数指针的typedef。
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、函数调用是后缀表达式,后面跟着括号,其中包含一个可能为空的逗号分隔的初始化器列表,这些列表构成函数的参数。后缀表达式应具有函数类型或函数指针类型。
因此,您也可以像下面这样定义函数:
void f( void h() ) { ( *h )(); }

甚至像这样:
void f( void h() ) { ( ******h )(); }

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接