c++ - 函数指针在C和C++中都立即返回吗?

标签 c++ c pointers asynchronous function-pointers

假设我想实现具有异步行为的函数,或者我只想使用函数指针,那么调用函数指针是否被授权导致调用关联函数,紧接着调用下一条指令?
例子

#include <iostream>
#include <cstdint>
int triple(int a) { return a * 3; }
void foo() { std::cout << "executing foo()" << '\n'; }
using fptrT = int (*)(int);
int main()
{
    fptrT p = triple;
    p(3);
    foo();
}

这两个标准对表达式p(3)求值和foo()执行的时间有何解释?

最佳答案

Christope's answer是正确的。这是补充。
函数指针本身不能提供异步行为。标准实际上禁止这样做。我对C标准比C++标准要熟悉得多,所以我会使用它。我的理解是,在这一点上,两者都应该是大致相同的。
C11标准对函数和函数指针的描述
让我们从C中函数调用的定义开始,见6.5.2.2第3段:
postfix表达式后跟括号()是一个函数调用,该表达式可能包含一个空的、逗号分隔的表达式列表。postfix表达式表示被调用的函数。表达式列表指定函数的参数。
并由第1段中的约束修改:
表示被调用函数(92)的表达式应具有指向返回void或返回数组类型以外的完整对象类型的函数的类型指针。
重要的是,随附的脚注92指出:
通常,这是转换作为函数指示符的标识符的结果。
因此,C11标准基本上将函数调用定义为调用函数指针的东西。并且,为此目的,命名函数标识符将自动转换为指向标识符中代码的函数指针。因此,C看不到函数和函数指针之间的区别。
实验
虽然参考标准总是很好的,但是看看可靠的实现是如何工作的也是非常有用的。让我们做一个测试,在这里我们编写相当简单的代码,然后查看底层程序集
代码:

#include <stdio.h>
#include <stdlib.h>

typedef void (*my_func_ptr)(int,int);

void my_function(int x, int y)
{
  printf("x = %d, y = %d, x + y = %d\n",x,y,x+y);
}

int main()
{
  /* declared volatile so the compiler has to call the function through
   * the pointer and cannot optimize it to call the function directly */
  volatile my_func_ptr fp = my_function; 

  my_function(3,5);
  fp(3,6);

  return 0;
}

我在Mac OS X上使用默认优化(gcc)来编译代码,这实际上是LLVM库的gcc前端。要查看程序集,我在gcc -o fptr fptr.c下运行程序,在lldb处设置断点,并发出main命令,该命令将反汇编当前函数。我将disassemble -f用于英特尔风格的程序集。settings set target.x86-disassembly-flavor intel中的默认值是a T&T样式,看起来有点不同。
程序集中的lldb例程如下:
  push   rbp
  mov    rbp, rsp
  sub    rsp, 0x20                   ; sets up the stack frame
  mov    edi, 0x3                    ; my_function(3,5). 1st arg: edi
  mov    esi, 0x5                    ;                   2nd arg: esi
  lea    rax, qword ptr [rip - 0x59] ; loads address of my_function into rax
  mov    dword ptr [rbp - 0x4], 0x0  
  mov    qword ptr [rbp - 0x10], rax ; saves address of my_function on stack
  call   0x100000ed0                 ; explicit call to my_function
  mov    eax, 0x0
  mov    edi, 0x3                    ; fp(3,6). 1st arg: edi
  mov    esi, 0x6                    ;          2nd arg: esi
  mov    rcx, qword ptr [rbp - 0x10] ; rcx <- address of my_function from stack
  mov    dword ptr [rbp - 0x14], eax
  call   rcx                         ; call address at rcx
  mov    eax, dword ptr [rbp - 0x14]
  add    rsp, 0x20
  pop    rbp
  ret    

注意,这两个函数调用本质上是相同的。他们使用同一个程序集。两次使用main操作调用实际调用。唯一的区别是第一次硬编码地址,第二次将地址存储在call寄存器中。还要注意的是,代码没有异步性。
C11所说的序列点
当您开始对序列点进行推理时,您实际上会发现,在单个线程中,标准不允许您期望的异步行为。在大多数情况下,C11约束编译器执行由序列点按顺序分隔的代码。在第5.1.2.3节(程序执行)中,程序的执行顺序定义为一系列序列点。相关定义基本上载于第3款:
前面的序列是由单个线程执行的计算之间的不对称、传递、成对关系,这会导致这些计算之间的部分顺序。给定任意两个评估A和B,如果A在B之前排序,则A的执行应先于B的执行。
在那一段后面:
表达式a和表达式B的求值之间存在一个序列点,这意味着与a相关联的每个值计算和副作用都在与B相关联的每个值计算和副作用之前排序。
基本上,这建立了由序列点分隔的代码必须同步(按顺序)执行。但是,如果编译器能够解释两段代码不能相互影响,则标准在第4段中提供了out:
在抽象机中,所有表达式都是按照语义指定的方式计算的。如果一个实际的实现能够推断出它的值没有被使用,并且没有产生所需的副作用(包括调用函数或访问易失性对象引起的任何副作用),那么它就不需要计算表达式的一部分。
那么,函数指针是如何输入的呢?附录C阐明了表达式语句之间的序列点,表达式语句本质上是以分号结尾的语句(见6.8.3)。这包括函数调用。
如何阻止函数指针的异步执行
考虑两个顺序函数调用:
f();
g();

两者都不需要争论,所以推理有点简单。调用必须按顺序执行,除非编译器可以解释rcx中未使用f()的任何副作用,反之亦然。编译器能够在函数中对此进行推理的唯一方法是,编译器是否可以使用函数的代码。通常,这对于函数指针是不可能的,因为指针可以指向满足函数指针类型约束的任何函数。
请注意,在某些情况下,编译器可以推断正确的函数(如果函数指针只被赋值一次并且存在于局部作用域中),但通常情况并非如此。因此,编译器必须按呈现的顺序执行函数,并且第一个函数必须在第二个函数之前返回。
线程和协同程序库怎么样
C11标准对线程有不同的规则。请注意,第5.1.2.3节将其自身限制为在单个线程中执行。使用堆栈的协同程序库实质上破坏了C11机器模型,并绑定到一组特定的实现(即:不需要移植到任何C环境)。协同程序库本质上必须提供自己的一组顺序顺序保证。

关于c++ - 函数指针在C和C++中都立即返回吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26918903/

相关文章:

c - 为什么即使我得到了正确的结果,程序还是崩溃了?

c++ - 在实时 Fedora 上编译 C++ 程序的命令

c++ - cpp文件中函数的顺序

c++ - "rvalue references for *this"是做什么用的?

c++ - 继承类中的 shared_from_this() 类型错误(是否有 dyn.type-aware 共享指针?)

c - 使用第二个参数的 ReadConsoleOutputCharacter 错误

c - 在C中动态分配内存给常量char指针?

c - 在函数中使用两次时 strtol 函数的异常行为

c - char s[] 和 char *s 在初始化方面有什么区别?

复制构造函数中的 C++ vector 数组