共用方式為


內嵌函式 (C++)

inline 關鍵詞建議編譯器取代函式定義內的程式碼,以取代該函式的每個呼叫。

理論上,使用內嵌函式可以讓程式更快速,因為它們會消除與函式呼叫相關聯的額外負荷。 呼叫函式需要推送堆疊上的傳回位址、將引數推送至堆疊、跳至函式主體,然後在函式完成時執行傳回指令。 內嵌函式可消除此流程。 編譯器也有不同的機會來最佳化展開的內嵌函式與非展開的內嵌函式。 內嵌函式的取捨是程式的整體大小可能會增加。

編譯器會判斷是否進行內嵌程式碼替代。 例如,如果函式的位址被取用,或編譯器認為函式太大,編譯器就不會內聯化。

inline關鍵字與單一定義規則(ODR)

該詞 inline 的原始含義是暗示編譯器偏好在呼叫站點展開程式碼,而非函式呼叫指令。 這仍然是 的 inline其中一個意義。

然而,這個 inline 關鍵字同時也對單一定義規則(One Definition Rule,ODR)有影響。 通常,函數只能在所有平移單元間定義一次。 當函式標記 inline為 時,若所有定義相同,則可在多個轉換單元中定義(通常透過標頭檔)。 連結器會選擇一個定義並丟棄重複的定義,而非報告錯誤。

這種同時作為優化提示與 ODR 機制的雙重性質 inline,可能會造成混淆。 ODR 特性是實際必要,當同一標頭(包含內嵌函式定義)可同時包含於多個原始碼檔案中時。

隱含內聯函數

某些函式隱含地不 inline 需關鍵字:

  • 在類別作用域定義的函式:在類別宣告主體中定義的函式隱含為內聯函式。 這使得小型存取器函式和能直接在類別定義中定義,且不會產生函式呼叫開銷——這是自 C++ 早期以來的優先事項。

  • constexpr 函數:宣 constexpr 告的函式(在 C++11 中引入)隱含 inline為 。 由於 constexpr 函式通常在標頭檔中定義以便編譯時評估,因此必須遵循與內嵌函式相同的 ODR 規則。

  • consteval 函式:宣 consteval 告的函式(在 C++20 中引入)隱含 inline為 。

內嵌變數(C++17)

C++17 將關鍵字擴展 inline 至變數。 變 inline 數可以在多個平移單元中定義,且如同串聯函數,連結器會選擇一個定義並捨棄其餘。

內嵌變數對於定義標頭檔中的常數或靜態資料成員非常有用:

// constants.h
inline constexpr double pi = 3.14159265358979323846;

struct MyClass
{
    static inline int instanceCount = 0;  // Can be defined in header
};

在 C++17 之前,這類變數需要在單一原始檔案中獨立定義,以避免連結錯誤。

範例:內聯類別成員函數

在接下來的類別宣告中, Account 建構子是一個內聯函式,因為它在類別宣告的正文中定義了。 成員函式 GetBalanceDepositWithdraw 會在其定義中指定 inlineinline 關鍵詞在類別宣告的函式宣告中為選擇性。

// 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;
}

注意

在類別宣告中,函式不會使用 inline 關鍵字宣告。 inline 關鍵字可以在類別宣告中指定,其結果相同。

指定的內嵌成員函式在每個編譯單位中必須以相同方式宣告。 內嵌函式必須只有一個定義。

除非類別成員函式的定義包含 inline 規範,否則該函式會預設為外部連結。 上述範例顯示您不需要使用 inline 規範明確宣告這些函式。 在函式定義中使用 會 inline 建議編譯器將其視為內嵌函式。 不過,您無法在呼叫該函式之後將函式重新宣告為 inline

inline__inline__forceinline

inline__inline 規範建議編譯器將函式主體插入呼叫該函式的每個地方。

只有在編譯器自身的成本效益分析顯示為有利時,插入 (稱為內嵌展開內嵌) 動作才會發生。 內嵌展開用較大的程式碼大小將函式呼叫額外負荷降至最低。

__forceinline關鍵字(或屬性[msvc::forceinline])會凌駕成本效益分析,而依賴程式設計師的判斷。 使用 __forceinline 時請小心。 隨意亂用 __forceinline 可能會導致更大的程式碼,但效能效益卻微乎其微,或在某些情況下,甚至效能損失 (例如,因為較大可執行檔的分頁增加)。

編譯器會將內嵌展開選項和關鍵字視為建議, 不保證函式一定會內嵌。 您無法強制編譯器內嵌特定的函式,即使是使用 __forceinline 關鍵字。 以 /clr 編譯時,如果有安全性屬性套用至此函式,則編譯器不會內嵌函式。

為了與先前版本相容,和 分別是 __inline 和 (__forceinline兩個前線底線)的同義詞,除非指定編譯器選項/Za(Disable language extensions)。_forceinline_inline

inline 關鍵字會指示編譯器其慣用內嵌展開。 不過,編譯器可以忽略它。 在兩種情況下,可能發生這種情況:

  • 遞迴函式
  • 透過在轉譯單位其他地方的指標所參考的函式。

這些原因可能會干擾內嵌,其他原因也可能干擾內嵌,具體由編譯器決定。 不要依賴 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、/O1 或 /O2 編譯。
  • 函式為遞迴,而且未設定 #pragma inline_recursion(on)。 伴隨 pragma,遞迴函式內嵌至預設為 16 個呼叫的深度。 若要減少內嵌深度,請使用 inline_depth pragma。
  • 函式是虛擬的,也是以虛擬方式呼叫。 對虛擬函式的直接呼叫可以內嵌。
  • 程式使用函式的位址,而且此呼叫是透過函式指標進行的。 其位址已取用之函式的直接呼叫可以內嵌。
  • 函式也會使用 naked__declspec 修飾詞標記。

如果編譯器無法內嵌以 __forceinline 宣告的函式,它會產生層級 1 的警告,除了以下情況:

  • 函式是使用 /Od 或 /Ob0 編譯。 在這些情況下,不會有內嵌。
  • 函式會在外部定義、在內含文件庫或其他編譯單位中,或是虛擬呼叫目標或間接呼叫目標。 編譯器無法識別目前編譯單位中找不到的非內嵌程式碼。

遞迴函式可以用內嵌程式碼替代為 inline_depth pragma 所指定的深度,最多 16 個呼叫。 該深度之後,遞迴函式呼叫視為函式執行個體的呼叫。 由內嵌啟發學習法所檢查之遞迴函式的深度不能超過 16。 inline_recursion pragma 會控制目前擴張中函式的內嵌展開。 如需相關資訊,請參閱內嵌函式展開 (/Ob) 編譯器選項。

C++ 標準會定義一組常見的屬性。 它也允許編譯器廠商在廠商專屬(我們這裡是 msvc)命名空間中定義自己的屬性。 以下 Microsoft 專屬屬性可用來控制內嵌行為:

Microsoft 專用屬性用於控制內嵌行為

Attribute Meaning
[msvc::forceinline] 與 意義相同 __forceinline
[msvc::forceinline_calls] 可以放在語句或區塊上或之前,使內嵌啟發式強制內嵌該語句或區塊中的所有呼叫。
[msvc::flatten] 類似 [[msvc::forceinline_calls]]於 ,但會遞迴地強制內嵌套用範圍內的所有呼叫,直到沒有呼叫剩餘。
[msvc::noinline] 當置於函式宣告前時,與 具有相同的意義 __declspec(noinline)
[msvc::noinline_calls] 可以放在任何語句或區塊之前,以關閉該範圍內所有呼叫的內嵌操作。

END 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;
}

假設座標操作在這種類別的用戶端中是相對常見的作業,那麼將兩個存取函式 (前述範例中的 xy) 指定為 inline,通常可在以下方面省去額外負荷:

  • 函式呼叫 (包括參數傳遞以及將物件的位址放置在堆疊上)
  • 保留呼叫端的堆疊框架
  • 新堆疊框架設定
  • 傳回值通訊
  • 還原舊堆疊框架
  • 退回

內嵌函式 vs. 巨集

巨集與 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))) 來解決。
  • 內嵌函式受限於編譯器的語意處理,而前置處理器擴充巨集卻沒有同樣的權益。 巨集不是型別安全,而函式則是。
  • 做為引數傳遞至內嵌函式的運算式會評估一次。 在某些情況下,做為引數傳遞至巨集的運算式可以多次評估。 例如,請考慮下列項目:
#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))) 函式會被呼叫兩次。 這導致第二次叫用 increment 會傳回 6,因此運算式評估為 30。 任何包含副作用的運算式在巨集中使用時,都可能會影響結果,請檢查完全展開的巨集以檢查行為是否為預期。 相反地,如果使用內嵌函式 square,則只會呼叫函式 increment 一次,並取得 25 的正確結果。

另請參閱

noinline
auto_inline
[msvc::forceinline]
[msvc::forceinline_calls]
[msvc::flatten]
[msvc::nolinline]
[msvc::nolinline_calls]