之前學 C++ 的時候,總是學到 inline 關鍵字的用法是「提示編譯器優化」,但是 cppreference 上描述的 inline 關鍵字是用來「允許函數(或變數)有多個定義,只要處於不同的翻譯單元」的。我很好奇,就翻了一下 cppreference 上的編輯日誌:

Revision history of "cpp/language/inline"?

en.cppreference.com

然後發現,在 2012 年之前 inline 關鍵字的描述也是「提示編譯器優化」,那麼到底是從什麼時候開始 inline 關鍵字的定義變成了「允許多次定義」呢?是某個新標準重新定義了 inline 關鍵字,還是大家一直都誤解了呢?


inline 關鍵字起先的含義確實是內聯優化提示,用於引導編譯器將所修飾的、且滿足內聯條件的函數摺疊進調用處。內聯以後的函數不會產生彙編或二進位層面的實體,而且使得程序的執行速度更快。

但是我們清楚,現代編譯器已十分智能,早已具備在內聯與否之間做出最佳決策的能力。程序員使用 inline 去指示編譯器優化,反倒是班門弄斧。因此很多指導書中提示,inline 關鍵字只是一種優化指示,用 inline 修飾的函數並不一定會被內聯掉。事實上,現在很多編譯器早就我行我素,並不太 care 這個優化指示了。

談到優化提示,我們會想到另外一個關鍵字 register,用於指示變數的寄存器優化。這同樣是一個編譯器早就不鳥的優化。正因為如此,register 從 C++11 開始被標記為廢棄,從 C++17 開始正式被廢了。而為什麼 inline 還活了下來呢?

這就是題主所疑惑的,inline 關鍵字在今天的作用。

首先,我們得明白,哪些 C++ 語法層面的成員編譯以後是彙編或二進位層面的實體。比如,沒被內聯的函數是實體,你編譯時如果要求輸出彙編文件,你打開這個文件會發現能找到這個函數;而內聯掉的函數,則不是實體,在彙編中找不到它。再舉例,模板不是實體,用到時被實例化了才可能產生實體 (這裡說可能是因為,實例化後如果滿足內聯條件有一定幾率會被內聯掉),而且對不同的模板參數組合,還可能會產生多個實體。再比如,宏函數一般就不是實體,它在預編譯時被展開了。再舉例,類申明一般不是實體,在類內定義的方法一般是相當於有默認的 inline 關鍵字修飾的,所以不是實體;而類外的方法定義、靜態成員變數定義一般才是實體。

不是實體的 C++ 成員,可以直接寫在頭文件里,被多個源文件包含。這些源文件分別編譯,最後鏈接時,重複的二進位實體只保留一份,其餘的捨棄掉。我們要明白,啥時候允許重複的實體只保留一份,而啥時候會報鏈接歧義衝突呢?普通函數受歷史包袱的影響,是不允許鏈接時有多個具有相同簽名的實體出現的,否則鏈接時會報鏈接歧義錯誤,因為鏈接器不知道要保留哪個版本的實體。C++ 的模板,如果實例化成實體,是允許有重複實體的。鏈接器會從中隨機挑一個,其餘捨棄。至於為什麼這時候鏈接器就不報歧義了,對不起,不要問,人為規定。

(岔個題,上一段的內容想讀懂,你得明白 C/C++ 的編譯模型才行。C/C++ 中每個源文件才是一個編譯單元,各個編譯單元在編譯期間互不了解其他單位裡面有什麼,只有在鏈接時才會互相有聯繫。我也是當時被鏈接階段各種花式報錯教過做人之後才理解的,都是淚啊都是淚啊 ??????????? )

扯得有點遠,回到 inline 上來。現在使用 inline 標記一個函數時,允許編譯單元中有相同簽名的實體,最後鏈接時只保留一個。這樣你就可以將函數的實現寫在頭文件里,而不用擔心衝突了。而如果沒有 inline 標記的函數,你把函數體寫在頭文件里,該頭文件被多個源文件包含並使用時,鏈接時就會衝突。

如果你打開純 C 語言的頭文件,會發現,裡面只有函數的申明,而沒有給出實現。這是因為,這些函數裡面的內容早已編譯成庫了,編譯的最後一個階段把庫鏈接進來就行。這種風格底下,頭文件只是起到公布介面的作用,缺點是使用不夠方便。而實現體放頭文件里,include 一下就能用,不用提前編譯好庫,不用管鏈接,這樣的風格逐漸受到了人們的歡迎,所以在這種趨勢下,inline 逐漸顯現出了這層語義。

題主現在已經知道了 inline 關鍵字的語義在今天已經發生了變化。但是具體是什麼時候開始有變化了呢?我只能說,從有編譯器允許加了 inline 的函數不會有鏈接衝突開始,這種轉變就已經開始發生了,而這是 C++98 標準形成以前的事情了。要知道,C++98 是 C++ 的第一個 ISO 標準。所以,標準化以前的歷史,具體糾結精確到什麼時候,就沒必要了。(多說一句,C++ 的社區氛圍很多時候是編譯器廠商、庫作者等用低層語法寫出高級語法的雛形,成熟以後再倒逼標準吸納這些概念並且進步的。很少有標準提出什麼新概念,然後編譯器才實現的。因此,標準化前的歷史,了解一下他的思想、了解一下他的發展過程就行,具體是什麼時候,興許早就有人做了吧)


Because the meaning of the keyword inline for functions came to mean "multiple definitions are permitted" rather than "inlining is preferred", that meaning was extended to variables. (since C++17)

很明顯,是C++17起含義變了。

不過我猜測這是個實現導致的標準改變。

在LTO不流行的年代,要實現多個c/cpp文件間的內聯是不可能的,所以需要內聯的函數都要被定義到頭文件。而一個函數如果定義到頭文件且不是static的,就會出現多個目標文件中出現同一個函數的情況,那這時允許這個inline函數在多個目標文件中被重複定義就是需要有的了。

而且,現在編譯器實現的默認都不按inline關鍵字做內聯,還有LTO可以跨目標文件內聯,inline本身的「偏好內聯」作用已經沒什麼效果和必要性了,所以乾脆就把標準改一下,只留下inline的副作用,也就是「允許在多個目標文件中被重複定義」了。


感覺樓上Tux ZZ的猜測應該是對的。inline建議內聯和允許重複定義就是雞和蛋的關係。估計以前的編譯器做不到鏈接期進行內聯優化,要內聯必須在編譯當前tu時看到這個函數完整的定義,所以這才允許inline修飾的函數的定義不同的tu中重複出現。


我還以為cppreference沒寫inline可以提示編譯器優化,結果明明是寫了嘛:

The original intent of the inline keyword was to serve as an indicator to the optimizer that inline substitution of a function is preferred over function call, that is, instead of executing the function call CPU instruction to transfer control to the function body, a copy of the function body is executed without generating the call. This avoids overhead created by the function call (copying the arguments and retrieving the result) but it may result in a larger executable as the code for the function has to be repeated multiple times.

Since this meaning of the keyword inline is non-binding, compilers are free to use inline substitution for any function thats not marked inline, and are free to generate function calls to any function marked inline. Those optimization choices do not change the rules regarding multiple definitions and shared statics listed above.


《C++語言的設計與演化》2.4就提到了inline


inline跟函數重載有什麼關係嗎,inline不是說執行某個類或者某個代碼塊時候把帶著inline的函數全部載入進內存嗎……


推薦閱讀:
相关文章