2つの質問があります。
1)インライン関数へのポインターがC++で許可されているのはなぜですか?インライン関数のコードは関数呼び出しステートメントにコピーされるだけで、インライン関数にはコンパイル時のメモリ割り当てがないことを読みました。それでは、インライン関数に固定メモリアドレスがないのに、なぜインライン関数へのポインタが存在できるのでしょうか?
2)以下のコードを検討してください。
_inline void func()
{
int n=0;
cout<<(&n);
}
_
func()
が呼び出されるたびにn
のアドレスの異なる値を出力すべきではありませんか? [インライン関数コードがコピーされるたびに、ローカル変数の再割り当てを行う必要があると思うので(通常の関数の場合、再初期化が行われます)]
私は初心者で、コンセプトを強化するためにこの質問をしました。どこか間違っている場合は修正してください。
1)インライン関数へのポインターがC++で許可されているのはなぜですか?
インライン関数は他の関数と同様の関数であり、それらを指すことは関数でできることの1つです。インライン関数は、この点で特別ではありません。
私は、インライン関数のコードを関数呼び出しステートメントにコピーするだけで、インライン関数にはコンパイル時のメモリ割り当てがないことを読みました。
あなた(そしておそらくあなたが読んだ資料)は、2つの関連する概念と同様の名前の概念を混在させています。
インライン関数は、それを使用するすべての翻訳単位で定義されますが、非インライン関数は、1つの定義ルールで必要な場合にのみ1つの翻訳単位で定義されます。それが関数のインライン宣言の意味です。 1つの定義ルールを緩和しますが、それを使用するすべての翻訳単位で定義されるという追加の要件も与えます(odrが緩和されていなければ不可能でした)。
インライン展開(またはインライン化)は最適化であり、呼び出された関数を呼び出し元のフレームにコピーすることにより、関数呼び出しが回避されます。関数がインラインで宣言されているかどうかにかかわらず、関数呼び出しはインラインで展開できます。そして、インラインで宣言された関数は、必ずしもインラインで展開されるとは限りません。
ただし、関数は、定義されていない翻訳ユニットでインラインに展開することはできません(リンク時最適化が展開を実行しない限り)。したがって、インライン宣言で許可されるすべてのTUで定義されるという要件は、関数を呼び出すすべてのTUで関数を定義できるようにすることで、関数のインライン展開も可能にします。ただし、最適化は保証されません。
2)func()が呼び出されるたびに、nの異なる値のアドレスを出力すべきではありませんか?
インライン展開により、ローカル変数は呼び出し元のフレームに配置されます(はい)。ただし、呼び出しが別々のフレームから発信された場合、それらの場所は展開に関係なく異なります。
通常、インラインで展開された関数の通常の非展開バージョンが生成されます。関数のアドレスが取得されると、その非拡張関数を指します。コンパイラーが関数のすべての呼び出しがインライン化されていることを証明できる場合、コンパイラーは非拡張バージョンをまったく提供しないことを選択する場合があります。これには、関数に内部リンクが必要であり、関数のアドレスを取得すると、通常、そのような証明が非常に困難または不可能になります。
inline
キーワード は元々、プログラマーがこの関数がインライン化の候補であるとあなたが考えるコンパイラーへのヒントでした-コンパイラーはこれを尊重する必要はありません。
現代の使用法では、インライン化とはまったく関係ありません-現代のコンパイラーは、「後ろに」関数を自由にインライン化(または非インライン化)します。これらは最適化手法の一部を形成します。
コード変換(インライン化を含む)は、C++の "as-if"ルール で行われます。これは基本的にコンパイラが実行が「as-if」である限り、元のコードが記述されたとおりに実行された限り、必要に応じてコードを変換できます。このルールは、C++の最適化を促進します。
ただし、関数のアドレスが取得されると、そのアドレスが存在する必要があります(つまり、アドレスは有効である必要があります)。これは、インライン化されていないことを意味する場合がありますが、それでも可能です(オプティマイザーは適切な分析を適用します)。
では、インライン関数の固定メモリアドレスがないのに、なぜインライン関数へのポインターが存在できるのでしょうか?
いいえ、これは単なるヒントであり、主にリンケージに関連しており、実際のインライン化には関連していません。これは、おそらく主な現在の使用法であり、ヘッダーファイルで関数を定義するものです。
func()
が呼び出されるたびにn
のアドレスの異なる値を出力すべきではありませんか?
n
は、関数の実行時のスタック位置に基づいたローカル変数である可能性があります。とは言っても、関数inline
はリンケージに関連し、リンカは変換ユニットに関数をマージします。
コメント ;で述べたように
...例が
static int n
に変更された場合、関数へのすべての呼び出しは(もちろん単一のプログラム実行で)定数値を出力する必要があり、コードがインライン化されているかどうかにかかわらずか否か。
これも、ローカル変数n
に対するリンケージ要件の影響です。
古い資料を読みます。現在inline
を使用する主な理由は、ヘッダーファイルで関数本体を許可するためです。関数でinline
キーワードを使用すると、変換ユニット全体の関数のすべてのインスタンスを結合できることをリンカーに通知します。複数のユニットからインクルードされるヘッダーに非インライン関数があると、One Definition Rule違反により未定義の動作が発生します。
C++ 17は インライン変数 も追加します。これは、ヘッダーで変数を定義できるのと同じプロパティを持ち、すべての定義はODR違反を引き起こす代わりにリンカーによって結合されます。
「呼び出し関数にコピーされるコード」で話しているものはinliningと呼ばれ、inline
キーワードとは無関係です。コンパイラーは、非インライン関数およびインライン関数に対して、最適化設定に基づいてこれを行うかどうかを決定します。
インライン関数は常にインライン化されるわけではありません。プログラマがこの関数のインライン化を希望していることを示すだけです。 コンパイラーは、インラインキーワードが使用されたかどうかに関係なく、関数をインライン化できます。
関数のアドレスが使用されている場合、少なくともGCCでは、関数は最終的な実行可能ファイルにインライン化されていない可能性があります。
関数がインラインと静的の両方である場合、関数へのすべての呼び出しが呼び出し元に統合され、関数のアドレスが使用されない場合、関数自体のアセンブラーコードは参照されません。
inline
関数を実際にインライン化する必要はないという既に述べた点とは別に(およびinline
areなしの多くの関数は、最新のコンパイラーによってインライン化されます)、インライン関数ポインタを介した呼び出し。例:
_#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などの言語用のコンパイラは、この種の最適化を大いに活用します。
†免責事項:私のアセンブリスキルはほとんど存在しません。私はここでゴミを話しているかもしれません。