为什么允许指向内联函数的指针

Why are pointers to inline functions allowed?

本文关键字:函数 指针 许指 为什么      更新时间:2023-10-16

我有两个问题:

1) 为什么C++中允许指向内联函数的指针?我读到过内联函数的代码只是被复制到函数调用语句中,并且内联函数中没有编译时内存分配。既然内联函数没有固定的内存地址,为什么可以存在指向内联函数的指针呢?

2) 考虑以下代码:

inline void func()    
{
int n=0;
cout<<(&n);
} 

不应该在每次调用func()时打印n地址的不同值吗?[因为我认为每次复制内联函数代码时,都必须重新分配局部变量(而在正常函数的情况下,则会进行重新初始化)]

我是一个初学者,我问这个问题是为了强化我的概念。如果我哪里错了,请纠正我。

1)为什么c++中允许指向内联函数的指针?

因为内联函数和其他函数一样,指向它们是使用函数可以做的事情之一。内联函数在这方面并不特别。

我读过内联函数的代码只是被复制到函数调用语句中,并且内联函数中没有编译时内存分配。

您(也许还有您阅读过的材料)混合了两个相关且名称相似的概念。

内联函数在所有使用它的翻译单元中定义,而非内联函数仅在一个翻译单元中根据一个定义规则的要求定义。这就是函数的内联声明的含义;它放宽了一个定义规则,但也提出了在所有使用它的翻译单元中定义的额外要求(如果odr没有放宽,这是不可能的)。

内联扩展(或内联)是一种优化,通过将调用的函数复制到调用方的框架中来避免函数调用。函数调用可以内联扩展,无论函数是否已内联声明。并且已经声明为内联的函数不一定是内联扩展的。

但是,函数不能在未定义的翻译单元中内联扩展(除非链接时间优化执行扩展)。因此,在内联声明允许的所有TU中定义的要求,也使得函数的内联扩展成为可能,因为允许在调用函数的所有TUs中定义函数。但优化并不能得到保证。

2)是否应该在每次调用func()时都打印n地址的不同值?

内联扩展确实会导致局部变量位于调用程序的框架中,是的。但是,如果呼叫来自不同的帧,则无论扩展如何,它们的位置都会有所不同。

通常,任何已内联扩展的函数都会生成一个常规的非扩展版本。如果取函数的地址,它将指向未展开的函数。如果编译器能够证明对函数的所有调用都是内联的,那么编译器可能会选择根本不提供非扩展版本。这要求函数具有内部链接,而获取函数的地址通常会使这种证明变得非常困难或不可能。

inline关键字最初是向编译器提示,程序员认为此函数是内联的候选函数-编译器不需要遵守这一点。

在现代用法中,它与内联几乎没有关系——现代编译器"在背后"自由内联(或不内联)函数,这些都是优化技术的一部分。

代码转换(包括内联)是在C++中的"好像"规则下完成的,这基本上意味着编译器可以随心所欲地转换代码,只要执行"好像"原始代码是按照编写的方式执行的。该规则促进了C++中的优化。

也就是说,一旦一个地址取自一个函数,它就必须存在(即该地址必须有效)。这可能意味着它不再内联,但仍然可以(优化器将应用适当的分析)。

既然内联函数没有固定的内存地址,为什么可以存在指向内联函数的指针?

不,它只是一个提示,在很大程度上与链接有关,而不是实际的内联。这为定义头文件中的函数提供了燃料,这可以说是当前的主要用途。

是否应该在每次调用func()时打印不同的n地址值?

n可能是一个局部变量,基于函数执行时的堆栈位置。也就是说,函数inline,它涉及链接,链接器将通过翻译单元合并函数。


如评论中所述;

。。。如果将示例更改为static int n,则对该函数的每次调用都必须打印一个常数值(当然是在单个程序运行中)。。。无论代码是否内联,这都是正确的。

这也是链接需求对局部变量n的影响。

您阅读了旧材料。现在使用inline的主要原因是允许在头文件中使用函数体。inline关键字与函数的使用向链接器发出信号,表明跨翻译单元的函数的所有实例都可以被组合;在多个单元中包含的标头中具有非内联函数会由于违反"一个定义规则"而导致未定义的行为。

C++17还添加了内联变量,这些变量具有与可以在头中定义变量相同的属性,并且所有定义都由链接器组合,而不是导致ODR冲突。

您所说的"代码被复制到调用函数"称为内联,与inline关键字无关。编译器将根据优化设置来决定是否对非内联函数和内联函数执行此操作。

内联函数并不总是内联的。它只是表示程序员希望这个函数内联。编译器可以内联任何函数,无论是否使用了内联关键字。

如果使用了函数的地址,那么该函数很可能没有内联在最终可执行文件中,至少在GCC:中是这样

当一个函数既是内联函数又是静态函数时,如果对该函数的所有调用都集成到调用方中,并且从未使用过该函数的地址,那么该函数自己的汇编代码也从未被引用。

GCC文档

除了已经说过的inline函数实际上不需要内联(许多没有inline的函数被现代编译器内联)之外,通过函数指针内联调用也是完全可以想象的。示例:

#include <iostream>
int foo(int (*fun)(int), int x) {
return fun(x);
}
int succ(int n) {
return n+1;
}
int main() {
int c=0;
for (int i=0; i<10000; ++i) {
c += foo(succ, i);
}
std::cout << c << std::endl;
}

这里,foo(succ, i)可以作为一个整体内联到i+1。事实上,这似乎发生了g++ -O3 -Sfoosucc函数生成代码

_Z3fooPFiiEi:
.LFB998:
.cfi_startproc
movq    %rdi, %rax
movl    %esi, %edi
jmp *%rax
.cfi_endproc
.LFE998:
.size   _Z3fooPFiiEi, .-_Z3fooPFiiEi
.p2align 4,,15
.globl  _Z4succi
.type   _Z4succi, @function
_Z4succi:
.LFB999:
.cfi_startproc
leal    1(%rdi), %eax
ret
.cfi_endproc

但随后它生成了main的代码,从未引用过中的任何一个,而是只包括一个新的专用_GLOBAL__sub_I__Z3fooPFiiEi:

.LFE999:
.size   _Z4succi, .-_Z4succi
.section    .text.startup,"ax",@progbits
.p2align 4,,15
.globl  main
.type   main, @function
main:
.LFB1000:
.cfi_startproc
movdqa  .LC1(%rip), %xmm4
xorl    %eax, %eax
pxor    %xmm1, %xmm1
movdqa  .LC0(%rip), %xmm0
movdqa  .LC2(%rip), %xmm3
jmp .L5
.p2align 4,,10
.p2align 3
.L8:
movdqa  %xmm2, %xmm0
.L5:
movdqa  %xmm0, %xmm2
addl    $1, %eax
paddd   %xmm3, %xmm0
cmpl    $2500, %eax
paddd   %xmm0, %xmm1
paddd   %xmm4, %xmm2
jne .L8
movdqa  %xmm1, %xmm5
subq    $24, %rsp
.cfi_def_cfa_offset 32
movl    $_ZSt4cout, %edi
psrldq  $8, %xmm5
paddd   %xmm5, %xmm1
movdqa  %xmm1, %xmm6
psrldq  $4, %xmm6
paddd   %xmm6, %xmm1
movdqa  %xmm1, %xmm7
movd    %xmm7, 12(%rsp)
movl    12(%rsp), %esi
call    _ZNSolsEi
movq    %rax, %rdi
call    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
xorl    %eax, %eax
addq    $24, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE1000:
.size   main, .-main
.p2align 4,,15
.type   _GLOBAL__sub_I__Z3fooPFiiEi, @function
_GLOBAL__sub_I__Z3fooPFiiEi:
.LFB1007:
.cfi_startproc
subq    $8, %rsp
.cfi_def_cfa_offset 16
movl    $_ZStL8__ioinit, %edi
call    _ZNSt8ios_base4InitC1Ev
movl    $__dso_handle, %edx
movl    $_ZStL8__ioinit, %esi
movl    $_ZNSt8ios_base4InitD1Ev, %edi
addq    $8, %rsp
.cfi_def_cfa_offset 8
jmp __cxa_atexit
.cfi_endproc
.LFE1007:
.size   _GLOBAL__sub_I__Z3fooPFiiEi, .-_GLOBAL__sub_I__Z3fooPFiiEi
.section    .init_array,"aw"
.align 8
.quad   _GLOBAL__sub_I__Z3fooPFiiEi
.local  _ZStL8__ioinit
.comm   _ZStL8__ioinit,1,1

因此,在这种情况下,实际程序甚至不包含指向succ的函数指针——编译器发现,无论如何,这个指针都会指向同一个函数,因此能够在不改变行为的情况下消除整个事件。当您经常通过函数指针调用小函数时,这可以大大提高性能。这在函数语言中是一种相当普遍的技术;O’Caml和Haskell等语言的编译器充分利用了这种优化。


免责声明:我的组装技能几乎不存在。我可能在这里胡说八道