インライン関数 (C++)

キーワード (keyword)はinline、コンパイラがその関数の各呼び出しの代わりに関数定義内のコードを置き換える必要があることを示唆しています。

理論的には、インライン関数を使用すると、関数呼び出しに関連するオーバーヘッドが排除されるため、プログラムを高速化できます。 関数を呼び出すには、スタック上のリターン アドレスをプッシュし、引数をスタックにプッシュし、関数本体にジャンプしてから、関数が終了したときに戻り命令を実行する必要があります。 このプロセスは、関数のインライン化によって排除されます。 コンパイラには、インラインで展開された関数と、展開されていない関数を最適化する機会も異なります。 インライン関数のトレードオフは、プログラムの全体的なサイズが増加する可能性があるということです。

インライン コードの置換は、コンパイラの判断で行われます。 たとえば、アドレスが取得された場合、またはコンパイラが大きすぎると判断した場合、コンパイラは関数をインライン化しません。

クラス宣言の本体で定義されている関数は、暗黙的にインライン関数です。

次のクラス宣言では、 Account コンストラクターはクラス宣言の本体で定義されているため、インライン関数です。 メンバー関数 GetBalance、、およびWithdrawそれらの定義で指定inlineDepositされます。 inline キーワード (keyword)は、クラス宣言の関数宣言では省略可能です。

// 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 キーワードは費用対効果分析をオーバーライドし、代わりにプログラマの判断に委ねられます。 __forceinline を使用する場合は注意が必要です。 __forceinline を無差別に使用すると、パフォーマンスがわずかに向上するだけで、コードが大きくなる可能性があります。場合によっては、パフォーマンスが低下することさえあります (たとえば、実行可能ファイルが大きくなってページングが増えるため)。

インライン展開に関するオプションとキーワードは、インライン展開の対象となる候補をコンパイラに示すだけです。 関数がインライン展開される保証はありません。 __forceinline キーワードを指定しても、特定の関数をコンパイラに強制的にインライン展開させることはできません。 コンパイル /clr時に関数にセキュリティ属性が適用されている場合、コンパイラは関数をインライン化しません。

以前のバージョンとの互換性を確保するため、_inline_forceinline は、コンパイラ オプション /Za (言語拡張機能の無効化) が指定されていない限り、それぞれ __inline__forceinline の同意語です。

inline キーワードは、インライン展開を優先することをコンパイラに指示します。 ただし、コンパイラでは無視できます。 このような動作になるのは、次の 2 つの場合です。

  • 再帰関数。
  • 翻訳単位の別の場所にあるポインターを通じて参照される関数。

これらの理由は、 コンパイラによって決定される他の理由と同様に、インライン化を妨げる可能性があります。 関数が inline インライン化されるように指定子に依存しないでください。

コンパイラは、ヘッダー ファイルで定義されているインライン関数を展開するのではなく、複数の翻訳単位で呼び出し可能な関数として作成できます。 コンパイラは、リンカーに対して生成された関数をマークして、1 定義規則 (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) コンパイラ オプションの説明を参照してください。

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 つのアクセサー関数 (前の例の xy) を inline と指定すると、通常、次のオーバーヘッドを削減できます。

  • 関数呼び出し (パラメーターの引き渡しおよびスタックへのオブジェクトのアドレスの配置を含む)
  • 呼び出し元のスタック フレームの保持
  • 新しいスタック フレームのセットアップ
  • 戻り値のやり取り
  • 古いスタックフレームの復元
  • Return

インライン関数とマクロ

マクロには、関数と 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) は 11 に評価されるまで拡張 2 + 2 * 3 + 3 されますが、予想される結果は 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

この例では、式sqr(increment(c))の展開に合わせて関数incrementが 2 回呼び出されます((increment(c)) * (increment(c)))。 これにより、2 回目の呼び出しで increment 6 が返されるため、式は 30 に評価されます。 副作用を含む式は、マクロで使用すると結果に影響を与える可能性があります。動作が意図されている場合は、完全に展開されたマクロを調べてチェックします。 代わりに、インライン関数が使用された場合、関数squareincrementは 1 回だけ呼び出され、25 の正しい結果が取得されます。

関連項目

noinline
auto_inline