摘要訊息 : inline 的另一種含義與內嵌變數.

0. 前言

對於 inline 這個關鍵字, 我們之前的解釋一直是讓函式內嵌. 確實, 這個含義才是 inline 關鍵字的本意. 但是實際上, 對於現代編碼器來說, 你為某個函式標識或者不標識 inline, 最終的結果其實都一樣. 也就是說, 優化能力比較強的 C++ 編碼器在這方面有些我行我素, 不太聽從程式設計師的 inline 建議. 這有些類似於 C++ 從 C 中繼承的 autoregister 關鍵字. 這兩個關鍵字最開始也是建議, 而現代編碼器早就對變數應該存放在哪裡有自己的優化方法, 從而自然不太關心程式設計師給出的建議. 正是編碼器的這種行為, 導致 C++ 遺棄了 autoregister 本來的含義. 現在, inline 在 C++ 中已經也是另外一種意思. 正是編碼器在 inline 上的我行我素, 才導致 C++ 標準對 inline 的主要含義進行了更改, 因為原來的含義已經沒有什麼意義了. 在講述內嵌變數之前, 我們首先來介紹 inline 的另一個含義.

更新紀錄 :

  • 2022 年 6 月 14 日進行第一次更新和修正.

1. 另一種 inline

假設現在在 1.hpp 中實作函式 f, 並且在 a.cppb.cpp 中同時匯入 1.hpp. 很顯然, 最終的結果是產生連結錯誤. 因為函式 f 同時在 a.cppb.cpp 中被定義了兩次, 編碼器在處理函式 f 的時候並不知道要連結 a.cpp 中的那個還是 b.cpp 中的那個. 如果僅僅在 1.hpp 中對函式 f 進行宣告, 無論函式 f 是否被 inline 標識, 只要在一份 .cpp檔案中實作, 這就不會導致連結錯誤. 這種分離實作方法是被大家廣泛接受的. 但是如果為函式 f 標識 inline, 即使在 1.hpp 中進行實作, 那麼這個連結錯誤也會被消除. 如果採用分離實作的方法, 在 1.hpp 中宣告函式 f (不論是否標識 inline), 然後在 a.cppb.cpp 中分別實作函式 f, 並且為其標識 inline, 現在再進行編碼也不會有連結錯誤. 就算是函式 fa.cppb.cpp 中的實作是完全不同的, 編碼器也不會擲出編碼錯誤或者連結錯誤. 這就要說到 inline 的另外一個含義 : 允許函式進行多次定義, 只要這些函式的定義的時候都被 inline 標識. 如果 a.cppb.cpp 的對於函式 f 的實作不同, 編碼器同時啟用兩份實作, 而是從全部實作中選擇一份, 其它的都被丟棄. 因此, 如果有這樣的程式碼 :

int f();
#include "header.hpp"

inline int f() {
    return 1;
}

auto a {f()};
#include "1.hpp"

inline int f() {
    return 2;
}

auto b {f()};
#include <iostream>
#include "1.hpp"

int main(int argc, char *argv[]) {
    extern int a, b;
    std::cout << a << b << f() << std::endl;
}

最終的輸出結果並非 121, 122 或者其它, 而是 111 或者 222. 在這裏, 因為 Clang 啟用了 a.cpp 中的那個版本, 而棄用了 b.cpp 中的那個版本, 因此結果為 111. 由 inline 的另一個含義, 我們必須知道, 被編碼器選中的那個實作版本是所有檔案共享的. 因此, 不論在任何檔案下, 函式 f 的地址都是相同的. 另外, 如果函式 f 中存在靜態變數, 這個靜態變數也是被所有檔案共享的. 但是, inline 導致的可重複定義的要求是函式型別必須相同, 否則也就沒有什麼重複定義的問題了...

2. 內嵌變數

C++ 17 提案 P0386R2《Inline Variables》要解決的問題就是將 inline 的可重複定義的特性帶到變數上面來. 從而在 C++ 17 之後, 變數也有了可重複定義的特性. 而且對於可重複定義這個含義, inline 在函式上如何表現, 那麼就在變數上如何表現.

因為編碼器在函式內嵌上的優化過於自信, 並且確實也不太需要程式設計師主動去為函式標識 inline 來決定函式是否內嵌, 於是 inline 在 C++ 17 之後的第一含義應該是決定某個函式或者變數是否可以被多次定義, 而非建議編碼器對函式進行內嵌.

最後, 大家需要注意, inline 的可重複定義這個含義自 C++ 98 就存在, 而並非 C++ 17 才引入的. 這個含義主要也是來自於內嵌函式這個含義, 或者稱為內嵌函式這個含義的副作用. 所謂內嵌函式就是在被呼叫處像巨集一樣展開, 避免函式呼叫的額外開銷. 於是, 一個內嵌函式必須保證自己的程式碼可以被複製多次, 從而要求不同檔案下的地址都要相同.