內嵌函式 (C++)
inline
關鍵詞建議編譯器取代函式定義內的程式碼,以取代該函式的每個呼叫。
理論上,使用內嵌函式可以讓程式更快速,因為它們會消除與函式呼叫相關聯的額外負荷。 呼叫函式需要推送堆疊上的傳回位址、將引數推送至堆疊、跳至函式主體,然後在函式完成時執行傳回指令。 內嵌函式可消除此流程。 編譯器也有不同的機會來最佳化展開的內嵌函式與非展開的內嵌函式。 內嵌函式的取捨是程式的整體大小可能會增加。
編譯器會判斷是否進行內嵌程式碼替代。 例如,如果函式的位址已被佔用,或編譯器認爲函式太大無法內嵌,則編譯器不會內嵌函式。
在類別宣告的主體中定義的函式隱含為內嵌函式。
範例
在下列類別宣告中,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;
}
注意
在類別宣告中,函式不會使用 inline
關鍵字宣告。 inline
關鍵字可以在類別宣告中指定,其結果相同。
指定的內嵌成員函式在每個編譯單位中必須以相同方式宣告。 內嵌函式必須只有一個定義。
除非類別成員函式的定義包含 inline
規範,否則該函式會預設為外部連結。 上述範例顯示您不需要使用 inline
規範明確宣告這些函式。 在函式定義中使用 inline
會建議編譯器將其視為內嵌函式。 不過,您無法在呼叫該函式之後將函式重新宣告為 inline
。
inline
、__inline
和 __forceinline
inline
和 __inline
規範建議編譯器將函式主體插入呼叫該函式的每個地方。
只有在編譯器自身的成本效益分析顯示為有利時,插入 (稱為內嵌展開或內嵌) 動作才會發生。 內嵌展開用較大的程式碼大小將函式呼叫額外負荷降至最低。
__forceinline
關鍵字會覆寫成本效益分析,並改為依賴程式設計人員的判斷。 使用 __forceinline
時請小心。 隨意亂用 __forceinline
可能會導致更大的程式碼,但效能效益卻微乎其微,或在某些情況下,甚至效能損失 (例如,因為較大可執行檔的分頁增加)。
編譯器會將內嵌展開選項和關鍵字視為建議, 不保證函式一定會內嵌。 您無法強制編譯器內嵌特定的函式,即使是使用 __forceinline
關鍵字。 以 /clr
編譯時,如果有安全性屬性套用至此函式,則編譯器不會內嵌函式。
為了與舊版相容,除非指定了編譯器選項 /Za
(停用語言延伸模組),否則 _inline
和 _forceinline
是 __inline
和 __forceinline
的同義字。
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) 編譯器選項。
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;
}
假設座標操作在這種類別的用戶端中是相對常見的作業,那麼將兩個存取函式 (前述範例中的 x
和 y
) 指定為 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
在這範例中,當運算式 sqr(increment(c))
展開至 ((increment(c)) * (increment(c)))
時,increment
函式會被呼叫兩次。 這導致第二次叫用 increment
會傳回 6,因此運算式評估為 30。 任何包含副作用的運算式在巨集中使用時,都可能會影響結果,請檢查完全展開的巨集以檢查行為是否為預期。 相反地,如果使用內嵌函式 square
,則只會呼叫函式 increment
一次,並取得 25 的正確結果。
另請參閱
意見反應
https://aka.ms/ContentUserFeedback。
即將登場:在 2024 年,我們將逐步淘汰 GitHub 問題作為內容的意見反應機制,並將它取代為新的意見反應系統。 如需詳細資訊,請參閱:提交並檢視相關的意見反應