inline キーワードは、コンパイラが関数の各呼び出しの代わりに、関数定義内のコードを置き換えることを示しています。
理論上は、インライン関数を使用すると、関数呼び出しに関連するオーバーヘッドが回避されるため、プログラムを高速化できます。 関数を呼び出すには、スタック上のリターン アドレスをプッシュし、引数をスタックにプッシュし、関数本体にジャンプしてから、関数が終了したときに戻り命令を実行する必要があります。 関数をインライン化することで、このプロセスがなくなります。 また、コンパイラには、インライン展開された関数と展開されていない関数を最適化するさまざまな機会があります。 インライン関数のトレードオフは、プログラムの全体的なサイズが増加する可能性があることです。
インライン コードを置き換えるかどうかは、コンパイラの判断で行われます。 たとえば、アドレスが取得された場合、またはコンパイラが大きすぎると判断した場合、コンパイラは関数をインライン化しません。
inline キーワードと One Definition Rule (ODR)
inlineの本来の意味は、関数呼び出し命令よりも呼び出しサイトでのコード拡張を好むコンパイラのヒントです。 これは、 inlineの意味の 1 つのままです。
ただし、 inline キーワードは、One Definition Rule (ODR) にも影響します。 通常、関数は、すべての翻訳単位で 1 回だけ定義できます。 関数が inlineマークされている場合、すべての定義が同一の場合、複数の翻訳単位 (通常はヘッダー ファイルを使用) で定義できます。 その後、リンカーは 1 つの定義を選択し、エラーを報告するのではなく、重複を破棄します。
最適化ヒントと ODR メカニズムの両方として、 inlineのこの二重の性質により、混乱が生じる可能性があります。 ODR の側面は、同じヘッダー (インライン関数定義を含む) が複数のソース ファイルに含まれる可能性がある、実用的な必要性です。
暗黙的にインライン関数
特定の関数は、キーワードを必要とせずに暗黙的に inline されます。
クラス スコープで定義された関数: クラス宣言の本体で定義されている関数は、暗黙的にインライン関数です。 これにより、小さなアクセサー関数を使用でき、関数呼び出しのオーバーヘッドを発生させることなく、クラス定義で直接定義できます。これは、C++ の初期の頃からの優先順位です。
constexpr関数:constexpr宣言された関数 (C++11 で導入) は暗黙的にinline。constexpr関数は通常、コンパイル時の評価を可能にするためにヘッダー ファイルで定義されるため、インライン関数と同じ ODR 規則に従う必要があります。consteval関数:consteval宣言された関数 (C++20 で導入) は暗黙的にinlineされます。
インライン変数 (C++17)
C++17 では、 inline キーワードが変数に拡張されました。
inline変数は複数の翻訳単位で定義でき、インライン関数と同様に、リンカーは 1 つの定義を選択し、残りの定義を破棄します。
インライン変数は、ヘッダー ファイルで定数または静的データ メンバーを定義する場合に便利です。
// constants.h
inline constexpr double pi = 3.14159265358979323846;
struct MyClass
{
static inline int instanceCount = 0; // Can be defined in header
};
C++17 より前では、リンカー エラーを回避するために、このような変数には 1 つのソース ファイルに個別の定義が必要がありました。
例: インライン クラスメンバー関数
次のクラス宣言では、 Account コンストラクターはクラス宣言の本体で定義されているため、インライン関数です。 メンバー関数 GetBalance、Deposit、および Withdraw は、定義で inline に指定されます。
inline キーワードは、クラス宣言の関数宣言では省略可能です。
// account.h
class Account
{
public:
Account(double initial_balance)
{
balance = initial_balance;
}
double GetBalance() const;
double Deposit(double amount);
double Withdraw(double amount);
private:
double balance;
};
inline double Account::GetBalance() const
{
return balance;
}
inline double Account::Deposit(double amount)
{
balance += amount;
return balance;
}
inline double Account::Withdraw(double amount)
{
balance -= amount;
return balance;
}
Note
クラス宣言では、関数は inline キーワードなしで宣言されていました。
inline キーワードをクラス宣言で指定できますが、結果は同じです。
特定のインライン メンバー関数は、すべてのコンパイル単位で同じ方法で宣言する必要があります。 インライン関数の定義は 1 つだけでなければなりません。
クラス メンバー関数は、その関数の定義に inline 指定子が含まれていない限り、既定で外部リンケージに設定されます。 前の例は、inline 指定子を使用してこれらの関数を明示的に宣言する必要がないことを示しています。 関数定義で inline を使用すると、インライン関数として処理することがコンパイラに提案されます。 ただし、関数を呼び出した後にその関数を inline として再宣言することはできません。
inline、__inline、__forceinline
inline 指定子と __inline 指定子は、関数本体のコピーを関数の各呼び出し位置に挿入するようコンパイラに示します。
挿入 ("インライン展開" または "インライニング" と呼ばれます) は、コンパイラ独自の費用対効果分析によってそれだけの価値があることがわかった場合にのみ行われます。 インライン展開では、関数呼び出しのオーバーヘッドが最小になりますが、コード サイズが大きくなるという潜在的なコストが発生します。
__forceinline キーワード (または [msvc::forceinline] 属性) は、コストメリット分析をオーバーライドし、代わりにプログラマの判断に依存します。
__forceinline を使用する場合は注意が必要です。
__forceinline を無差別に使用すると、パフォーマンスがわずかに向上するだけで、コードが大きくなる可能性があります。場合によっては、パフォーマンスが低下することさえあります (たとえば、実行可能ファイルが大きくなってページングが増えるため)。
インライン展開に関するオプションとキーワードは、インライン展開の対象となる候補をコンパイラに示すだけです。 関数がインライン展開される保証はありません。
__forceinline キーワードを指定しても、特定の関数をコンパイラに強制的にインライン展開させることはできません。
/clr を指定してコンパイルする場合、関数にセキュリティ属性が適用されていると、コンパイラは関数をインライン展開しません。
以前のバージョンとの互換性のために、コンパイラ オプション /Za(言語拡張機能を無効にする) が指定されていない限り、_inlineと_forceinlineはそれぞれ、__inlineと__forceinline (先頭に 2 つのアンダースコア) のシノニムです。
inline キーワードは、インライン展開を優先することをコンパイラに指示します。 ただし、コンパイラはこれを無視できます。 このような動作になるのは、次の 2 つの場合です。
- 再帰関数。
- 翻訳単位の別の場所にあるポインターを通じて参照される関数。
これらの理由は、他の要因と共に、インライン展開に関するコンパイラの判断に影響する可能性があります。 関数のインライン展開を inline 指定子で確実に行うことはできません。
コンパイラは、ヘッダー ファイルで定義されているインライン関数を展開するのではなく、複数の翻訳単位内で呼び出し可能な関数として作成できます。 コンパイラは、単一定義規則 (ODR) 違反を防止するために、リンカーに対して生成された関数をマークします。
通常の関数と同様に、インライン関数での引数の評価に対して定義された順序はありません。 実際には、通常の関数呼び出しプロトコルを使用して渡された場合の引数の評価順序とは異なる可能性があります。
インライン関数展開が実際に行われるかどうかに影響を与えるには、/Ob コンパイラ最適化オプションを使用します。
/LTCG は、ソース コードで要求されているかどうかにかかわらず、モジュール間のインライン展開を行います。
例 1
// inline_keyword1.cpp
// compile with: /c
inline int max(int a, int b)
{
return a < b ? b : a;
}
クラスのメンバー関数は、inline キーワードを使用するか、クラス定義内に関数定義を配置することにより、インラインとして宣言できます。
例 2
// inline_keyword2.cpp
// compile with: /EHsc /c
#include <iostream>
class MyClass
{
public:
void print() { std::cout << i; } // Implicitly inline
private:
int i;
};
Microsoft 固有の仕様
__inline キーワードは inline に相当します。
次の場合、__forceinline を使用しても、コンパイラは関数をインライン展開できません。
-
/Ob0(デバッグ ビルドの既定オプション) を指定して関数またはその呼び出し元がコンパイルされる。 - 関数と呼び出し元が、異なる種類の例外処理を使用する (一方は C++ 例外処理、他方は構造化例外処理)。
- 関数に可変個引数リストが含まれている。
-
/Ox、/O1、または/O2を指定してコンパイルされない限り、関数はインライン アセンブリを使用する。 - 関数が再帰的であり、
#pragma inline_recursion(on)が設定されていない。 プラグマによって、再帰関数はインライン展開され、既定の深さは 16 呼び出しになります。 インライン展開の深さを減らすには、inline_depthプラグマを使用します。 - 関数が仮想で、仮想的に呼び出される。 仮想関数への直接呼び出しはインライン展開できます。
- プログラムが関数のアドレスを受け取り、関数へのポインターによって呼び出される。 受け取られるアドレスを持っている関数への直接呼び出しはインライン展開できます。
- 関数に
naked__declspec修飾子も付けられている。
__forceinline を使用して宣言された関数をコンパイラがインライン展開できない場合、次の状況でない限り、レベル 1 の警告が生成されます。
- /Od または /Ob0 を使用して関数がコンパイルされる。 次のような場合、インライン展開が行われることはありません。
- 関数が外部 (組み込みライブラリ内または別の翻訳単位内) で定義されている、あるいは仮想呼び出しターゲットまたは間接呼び出しターゲットである。 現在の翻訳単位内で見つからない非インライン展開コードをコンパイラが特定できない。
再帰関数は、inline_depth プラグマで指定された深さ (最大 16 の呼び出し) までインライン コードに置き換えることができます。 その深さの後、再帰関数の呼び出しは、関数のインスタンスへの呼び出しとして扱われます。 再帰関数がインライン ヒューリスティックによってチェックされる深さは、16 を超えることはできません。
inline_recursion プラグマは、現在展開中の関数のインライン展開を制御します。 関連情報については、インライン関数の展開 (/Ob) コンパイラ オプションの説明を参照してください。
C++ 標準では、共通の属性セットが定義されています。 また、コンパイラ ベンダーは、ベンダー固有の (ここでは、 msvc) 名前空間内で独自の属性を定義することもできます。 次の Microsoft 固有の属性を使用して、インライン展開の動作を制御できます。
インライン展開の動作を制御するための Microsoft 固有の属性
| 特性 | Meaning |
|---|---|
[msvc::forceinline] |
__forceinlineと同じ意味を持ちます。 |
[msvc::forceinline_calls] |
ステートメントまたはブロックの前後に配置して、インラインヒューリスティックがそのステートメントまたはブロック内のすべての呼び出しを強制的にインライン化することができます。 |
[msvc::flatten] |
[[msvc::forceinline_calls]]に似ていますが、呼び出しが残らないまで、適用されるスコープ内のすべての呼び出しを再帰的に強制的にインライン化します。 |
[msvc::noinline] |
関数宣言の前に配置すると、 __declspec(noinline)と同じ意味を持ちます。 |
[msvc::noinline_calls] |
ステートメントまたはブロックの前に配置して、適用されるスコープ内のすべての呼び出しのインライン化を無効にすることができます。 |
Microsoft 固有の仕様はここまで
inline 指定子の使い方の詳細については、以下を参照してください。
インライン関数を使用する状況
インライン関数は、データ メンバーにアクセスできるようにする関数のような小さな関数に使用するのが最適です。 短い関数は、関数呼び出しのオーバーヘッドに影響を受けます。 長い関数は、呼び出すシーケンスと返すシーケンスにかかる時間が相対的に短く、インライン展開の利点が小さくなります。
Point クラスは次のように定義できます。
// when_to_use_inline_functions.cpp
// compile with: /c
class Point
{
public:
// Define "accessor" functions
// as reference types.
unsigned& x();
unsigned& y();
private:
unsigned _x;
unsigned _y;
};
inline unsigned& Point::x()
{
return _x;
}
inline unsigned& Point::y()
{
return _y;
}
このようなクラスの利用時は座標の操作は比較的一般的な操作であると仮定すれば、2 つのアクセサー関数 (前の例の x と y) を inline と指定すると、通常、次のオーバーヘッドを削減できます。
- 関数呼び出し (パラメーターの引き渡しおよびスタックへのオブジェクトのアドレスの配置を含む)
- 呼び出し元のスタック フレームの保持
- 新しいスタック フレームのセットアップ
- 戻り値のやり取り
- 古いスタックフレームの復元
- 返却
インライン関数とマクロ
マクロには、inline 関数との共通点がいくつかあります。 ただし、重要な違いがあります。 次の例を確認してください。
#include <iostream>
#define mult1(a, b) a * b
#define mult2(a, b) (a) * (b)
#define mult3(a, b) ((a) * (b))
inline int multiply(int a, int b)
{
return a * b;
}
int main()
{
std::cout << (48 / mult1(2 + 2, 3 + 3)) << std::endl; // outputs 33
std::cout << (48 / mult2(2 + 2, 3 + 3)) << std::endl; // outputs 72
std::cout << (48 / mult3(2 + 2, 3 + 3)) << std::endl; // outputs 2
std::cout << (48 / multiply(2 + 2, 3 + 3)) << std::endl; // outputs 2
std::cout << mult3(2, 2.2) << std::endl; // no warning
std::cout << multiply(2, 2.2); // Warning C4244 'argument': conversion from 'double' to 'int', possible loss of data
}
33
72
2
2
4.4
4
マクロとインライン関数の違いを次に示します。
- マクロは常にインラインで展開されます。 ただし、インライン関数は、コンパイラがそれが最適であると判断した場合にのみインライン化されます。
- マクロによって予期しない動作が生じて、特定しにくいバグが発生する可能性があります。 たとえば、式
mult1(2 + 2, 3 + 3)は2 + 2 * 3 + 3に展開され、11 に評価されますが、予想される結果は 24 です。 一見有効な修正は、関数マクロの両方の引数の周りにかっこを追加することです。その結果、#define mult2(a, b) (a) * (b)が発生します。これにより、目の前の問題は解決されますが、より大きな式の一部では驚くべき動作を引き起こす可能性があります。 これは前の例で示されており、#define mult3(a, b) ((a) * (b))としてマクロを定義することで問題に対処できました。 - インライン関数はコンパイラによるセマンティック処理の対象になりますが、プリプロセッサは同じメリットなしにマクロを展開します。 マクロはタイプ セーフではありませんが、関数はタイプ セーフです。
- インライン関数に引数として渡された式は、1 回だけ評価されます。 マクロでは、引数として渡された式が複数回評価される可能性があります。 たとえば、次のことを考慮してください。
#include <iostream>
#define sqr(a) ((a) * (a))
int increment(int& number)
{
return number++;
}
inline int square(int a)
{
return a * a;
}
int main()
{
int c = 5;
std::cout << sqr(increment(c)) << std::endl; // outputs 30
std::cout << c << std::endl; // outputs 7
c = 5;
std::cout << square(increment(c)) << std::endl; // outputs 25
std::cout << c; // outputs 6
}
30
7
25
6
この例では、式 increment が sqr(increment(c)) に展開されるので、関数 ((increment(c)) * (increment(c))) が 2 回呼び出されます。 これにより、increment の 2 回目の呼び出しで 6 が返されるため、式は 30 に評価されます。 副作用を含む式は、マクロで使用するときに結果に影響する可能性があります。完全に展開されたマクロを調べて、動作が意図されたものであるかどうかを確認します。 代わりに、インライン関数 square が使用された場合、関数 increment は 1 回だけ呼び出され、正しい結果の 25 が取得されます。
関連項目
noinline
auto_inline
[msvc::forceinline]
[msvc::forceinline_calls]
[msvc::flatten]
[msvc::nolinline]
[msvc::nolinline_calls]