web-dev-qa-db-ja.com

インライン関数へのポインターが許可されるのはなぜですか?

2つの質問があります。

1)インライン関数へのポインターがC++で許可されているのはなぜですか?インライン関数のコードは関数呼び出しステートメントにコピーされるだけで、インライン関数にはコンパイル時のメモリ割り当てがないことを読みました。それでは、インライン関数に固定メモリアドレスがないのに、なぜインライン関数へのポインタが存在できるのでしょうか?

2)以下のコードを検討してください。

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

func()が呼び出されるたびにnのアドレスの異なる値を出力すべきではありませんか? [インライン関数コードがコピーされるたびに、ローカル変数の再割り当てを行う必要があると思うので(通常の関数の場合、再初期化が行われます)]

私は初心者で、コンセプトを強化するためにこの質問をしました。どこか間違っている場合は修正してください。

50

1)インライン関数へのポインターがC++で許可されているのはなぜですか?

インライン関数は他の関数と同様の関数であり、それらを指すことは関数でできることの1つです。インライン関数は、この点で特別ではありません。

私は、インライン関数のコードを関数呼び出しステートメントにコピーするだけで、インライン関数にはコンパイル時のメモリ割り当てがないことを読みました。

あなた(そしておそらくあなたが読んだ資料)は、2つの関連する概念と同様の名前の概念を混在させています。

インライン関数は、それを使用するすべての翻訳単位で定義されますが、非インライン関数は、1つの定義ルールで必要な場合にのみ1つの翻訳単位で定義されます。それが関数のインライン宣言の意味です。 1つの定義ルールを緩和しますが、それを使用するすべての翻訳単位で定義されるという追加の要件も与えます(odrが緩和されていなければ不可能でした)。

インライン展開(またはインライン化)は最適化であり、呼び出された関数を呼び出し元のフレームにコピーすることにより、関数呼び出しが回避されます。関数がインラインで宣言されているかどうかにかかわらず、関数呼び出しはインラインで展開できます。そして、インラインで宣言された関数は、必ずしもインラインで展開されるとは限りません。

ただし、関数は、定義されていない翻訳ユニットでインラインに展開することはできません(リンク時最適化が展開を実行しない限り)。したがって、インライン宣言で許可されるすべてのTUで定義されるという要件は、関数を呼び出すすべてのTUで関数を定義できるようにすることで、関数のインライン展開も可能にします。ただし、最適化は保証されません。

2)func()が呼び出されるたびに、nの異なる値のアドレスを出力すべきではありませんか?

インライン展開により、ローカル変数は呼び出し元のフレームに配置されます(はい)。ただし、呼び出しが別々のフレームから発信された場合、それらの場所は展開に関係なく異なります。

通常、インラインで展開された関数の通常の非展開バージョンが生成されます。関数のアドレスが取得されると、その非拡張関数を指します。コンパイラーが関数のすべての呼び出しがインライン化されていることを証明できる場合、コンパイラーは非拡張バージョンをまったく提供しないことを選択する場合があります。これには、関数に内部リンクが必要であり、関数のアドレスを取得すると、通常、そのような証明が非常に困難または不可能になります。

58
eerorika

inlineキーワード は元々、プログラマーがこの関数がインライン化の候補であるとあなたが考えるコンパイラーへのヒントでした-コンパイラーはこれを尊重する必要はありません。

現代の使用法では、インライン化とはまったく関係ありません-現代のコンパイラーは、「後ろに」関数を自由にインライン化(​​または非インライン化)します。これらは最適化手法の一部を形成します。

コード変換(インライン化を含む)は、C++"as-if"ルール で行われます。これは基本的にコンパイラが実行が「as-if」である限り、元のコードが記述されたとおりに実行された限り、必要に応じてコードを変換できます。このルールは、C++の最適化を促進します。

ただし、関数のアドレスが取得されると、そのアドレスが存在する必要があります(つまり、アドレスは有効である必要があります)。これは、インライン化されていないことを意味する場合がありますが、それでも可能です(オプティマイザーは適切な分析を適用します)。

では、インライン関数の固定メモリアドレスがないのに、なぜインライン関数へのポインターが存在できるのでしょうか?

いいえ、これは単なるヒントであり、主にリンケージに関連しており、実際のインライン化には関連していません。これは、おそらく主な現在の使用法であり、ヘッダーファイルで関数を定義するものです。

func()が呼び出されるたびにnのアドレスの異なる値を出力すべきではありませんか?

nは、関数の実行時のスタック位置に基づいたローカル変数である可能性があります。とは言っても、関数inlineはリンケージに関連し、リンカは変換ユニットに関数をマージします。


コメント ;で述べたように

...例がstatic int nに変更された場合、関数へのすべての呼び出しは(もちろん単一のプログラム実行で)定数値を出力する必要があり、コードがインライン化されているかどうかにかかわらずか否か。

これも、ローカル変数nに対するリンケージ要件の影響です。

26
Niall

古い資料を読みます。現在inlineを使用する主な理由は、ヘッダーファイルで関数本体を許可するためです。関数でinlineキーワードを使用すると、変換ユニット全体の関数のすべてのインスタンスを結合できることをリンカーに通知します。複数のユニットからインクルードされるヘッダーに非インライン関数があると、One Definition Rule違反により未定義の動作が発生します。

C++ 17は インライン変数 も追加します。これは、ヘッダーで変数を定義できるのと同じプロパティを持ち、すべての定義はODR違反を引き起こす代わりにリンカーによって結合されます。

「呼び出し関数にコピーされるコード」で話しているものはinliningと呼ばれ、inlineキーワードとは無関係です。コンパイラーは、非インライン関数およびインライン関数に対して、最適化設定に基づいてこれを行うかどうかを決定します。

14
M.M

インライン関数は常にインライン化されるわけではありません。プログラマがこの関数のインライン化を希望していることを示すだけです。 コンパイラーは、インラインキーワードが使用されたかどうかに関係なく、関数をインライン化できます。

関数のアドレスが使用されている場合、少なくともGCCでは、関数は最終的な実行可能ファイルにインライン化されていない可能性があります。

関数がインラインと静的の両方である場合、関数へのすべての呼び出しが呼び出し元に統合され、関数のアドレスが使用されない場合、関数自体のアセンブラーコードは参照されません。

GCCドキュメント

5
Ville-Valtteri

inline関数を実際にインライン化する必要はないという既に述べた点とは別に(およびinlineareなしの多くの関数は、最新のコンパイラーによってインライン化されます)、インライン関数ポインタを介した呼び出し。例:

_#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 -S_は、fooおよびsucc関数のコードを生成します

__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などの言語用のコンパイラは、この種の最適化を大いに活用します。


免責事項:私のアセンブリスキルはほとんど存在しません。私はここでゴミを話しているかもしれません。

3
leftaroundabout