標籤:span 傳統 內嵌函式 內聯 獲得 comm inter func rup
constexpr函數是指能用於常量運算式的函數。定義constexpr函數的方法有其他函數類似,不過要遵循幾項約定:函數的傳回值類型及所以形參的類型都是字面實值型別,而且函數體中必須有且只有一條return語句。為了能在編譯過程中隨時展開,constexpr函數被隱式指定地指定為內嵌函式。
constexpr函數體內也可以包含其他語句,只要這些語句在運行時不執行任何操作就行。例如,constexpr函數中可以有空語句、類型別名以及using聲明。
允許constexpr函數的傳回值並非一個常量:
constexpr int scale(int cnt){return 5*cnt;}//如果arg是常量運算式,則scale(arg)也是常量運算式
當scale的實參是常量運算式時,它的傳回值也是常量運算式;反之則不然。如果我們用一個非常量運算式調用scale函數,比如int類型的對象i,則傳回值是一個非常量運算式。當把scale函數用在需要常量運算式的上下文時,由編譯器負責檢查函數的結果是否符合要求。如果結果恰好不是常量運算式,編譯器將發出錯誤資訊。constexpr函數不一定返回常量運算式。
把內嵌函式和constexpr函數放在標頭檔內
和其他函數不一樣,內嵌函式和constexpr函數可以在程式中多次定義。畢竟,編譯器要想展開函數僅有函式宣告是不夠的,還需要函數的定義。不過,對於某個給定的內嵌函式或者constexpr函數來說,它的多個定義必須完全一致,基於這個原因,內嵌函式和constexpr函數通常定義在標頭檔中。
https://www.cufe-ifc.org/question/153643.html
C++ 內嵌函式和constexpr函數可以在程式中定義不止一次,這個一般用在什麼時候?Tim Shen 2017-08-21 318 內嵌函式 c++(C++ Primer 第5版 215頁) 而且,書中有說:“對於某個給定的內嵌函式或者constexpr函數來說,它的多個定義必須完全一致。基於這個原因,內嵌函式和constexpr函數通常定義在標頭檔中”。 為什麼放在源檔案裡就不可以了呢? 我用網上的例子(淺談C++中內聯關鍵字inline)運行了一下: A.h #pragma once class A { public: A(int a, int b) : a(a), b(b) {} int max(); private: int a; int b; }; A.cpp #include "A.h" inlin… 0 0其他回答能定義不止一次的好處是方便你放到標頭檔裡,標頭檔裡的好處是每個include這個標頭檔的.c檔案都能函數體,看到函數體的好處是編譯器可以內聯。內聯的好處是代碼變快了。另外,所有函數體定義必須一模一樣,不然出了問題概不負責。constexpr內建inline屬性。
當你下決心在.c檔案中定義函數體的時候,自然不需要inline關鍵字了。而這時候也必須link相應的.o來確保找得到定義。
------------------------------ 話癆版 ------------------------------
首先來幾個前置知識:
1) C和C++都有translation unit(或compilation unit)的概念:基本上編譯器會一次編譯一個檔案,然後忘記一切,然後再編譯下一個檔案。哪怕你寫gcc -c a.c b.c,其實和gcc -c a.c && gcc -c b.c大體上是沒區別的。在最後,所有的.o檔案都被linker匯總link成一個大的可執行檔。
2) static function。你可以把一個函數標記為static(也稱為internal linkage),這樣該函數的symbol就會被隱藏,從而該函數只存在在當前translation unit。換了下一個translation unit之後,該函數被忘得一乾二淨。linker也從來不知道這函數存在過。這時候你就算再定義一次上次translation已經定義過的static函數,linker也不會報redefinition錯誤。當然,這樣代碼在binary中就出現了多次。
3) 當然你也肯定知道C和C++的include的意思:在A中#include <B>就是把B的內容複寫粘貼到A中#include對應的位置。
4) 編譯器的內聯最佳化就是看到你在Bar裡調用Foo的時候,幫你複製一遍Foo的函數體,內嵌到Bar裡去,同時消除棧操作的開銷(因為代碼已經被複製到“本地”了嘛,不需要跳來跳去了)。內聯最佳化有個缺陷,就是在同一個translation unit裡一定要看到函數體,所以光看到declaration是沒用的。
現在考慮這麼個問題:傳統的在標頭檔中聲明,在一個檔案(.c)中實現函數體的方式有時執行太慢了。為什麼慢呢,假設我這個函數就一行,但是函數調用的壓棧傳參數彈棧跳轉等指令佔了大部分開銷,真是太不合算了。
這時候在傳統C裡面有兩個解決方案:
1) “宏函數”。就是把本來藏在.c檔案裡的函數體放到一個宏裡面去,當然宏也在標頭檔裡。然後大家include標頭檔的時候就把宏也include走了,使用宏的時候就把這段代碼一遍遍展開到所有使用的地方,消除了函數調用的開銷。
2) 在編譯器支援內聯最佳化的情況下,在標頭檔裡定義static function。任何別的.c檔案,只要include了你的標頭檔,都對你的標頭檔做了一次複製粘貼,自動獲得了該static function的函數體。所以在不同的translation unit裡面,這些函數並不衝突,因為它們是static的。值得一提的是,這些函數體不一定一模一樣。舉例來說:
// a.h #define FOO 3static int Foo() { return FOO; }// a.c#include "a.h"// b.c#undef FOO#define FOO 2#include "a.h"
在不同的translation unit裡面一個Foo返回3一個返回2。
1) 的壞處很明顯,宏不能解決類型檢查的問題,宏是dynamic scope(變數檢查環境都是調用端而非定義端的)的,宏是textual substitution,搞不好有迷之編譯不通過,宏很醜,定義不帶文法高亮(霧),等等。
2) 看上去很好誒,寫的是真正的函數,編譯器還有能力內聯。其缺陷是在編譯器決定不內聯的時候(通常這時候函數很大),每個translation unit中都定義了一個很大的函數,造成了不必要的binary size bloat。
這時候C++之父Bjarne Stroustrup站出來了,說我們在C++裡搞個inline關鍵字吧!這個關鍵字不僅編譯器認識,而且編譯器在沒有真正內聯該函數時,會通過某種方式提示linker說這個函數被標記為“可重複定義”耶 - 根據我用gcc的實驗,產生的是一個weak symbol。當linker看到一個weak symbol,會把函數名寫在一個小本本上。在linker最後把所有檔案link到一起的時候,它會把小本本掃一遍,對於同名函數只留一個,別的函數連帶函數體統統刪掉。這樣就解決了binary size bloat的問題。當然這隻是一種典型實現方式,並非一定如此。
另外,在編譯器真正內聯了該函數的時候,效果就和static一樣了,這也是為什麼你的代碼裡找不到定義 - 因為linker根本看不到static函數。話雖這麼說,但是他們不管這個叫internal linkage(inline specifier),因為此時linkage是個implementation detail。語言只是強調:
The definition of an inline function must be present in the translation unit where it is called (not necessarily before the point of call)
在前面提到,用include static functions的方式中include進來的函數體可能不完全一樣。inline此處也提到,你要是同名函數的函數體長得不一樣,我才不告訴你我要留哪一份刪哪幾份呢。你要是敢這麼做,我不保證我的輸出有意義。這個在C++裡叫做ODR violation (Definitions and ODR)。編譯器一次只看一個translation unit,所以通常是沒法檢測ODR violation的(不排除LTO還是能查的),而linker也不查,我並不清楚為什麼,大概是太昂貴吧。
另外,可以感受下當年inline關鍵字的marketing口號:
“An Inline Function is As Fast As a Macro”(Inline - Using the GNU Compiler Collection (GCC))
順帶建議一下刷ACM-ICPC的各種坑爹oj的同學,想要速度就用宏,因為oj可能不開內聯最佳化。。Tim Shen 2017-08-21 14:33:18 0條評論 0 0對於內嵌函式而言,其比較大的特點就是會在函數調用的地方被直接展開,而不是一個函數調用,從而減少函數的調用開銷。而若是函數調用的話,我們可以在使用函數前的時候,如int foo()的方式,然後讓後面的連結器去其它的目標檔案進行函數符號的尋找。
我來仔細的一步一步的分析這裡面產生的緣由,我使用gcc作為示範,因為Linux有一系列的工具可以方便查看,但是這和Visual C++的原理是一致的。
回到你的例子,我們使用g++ -c http://main.cc -o main.o,然後使用nm main.o | c++filt 來查看產生的符號,如所示:
我們可以很清晰的看到A::max()的符號是U,即undefined,所以,它希望連結器去其它的地方找到函數的定義。若是一切正常的話,那麼我們還有一個A.cpp,然後產生一個a.o,連結器去a.o的地方就可以找到這個函數定義了。
然而我們去編譯其實現檔案查看其符號表:
我們可以發現,是沒有這個符號表的,因為如前文所述,inline函數並不會產生函數調用,會就在本源檔案中展開,於是也就沒有了函數的符號。
那麼正是如此,後面連結器,把產生的.o合并到一起的時候,A::max()依然是未定義的,如所示:
那麼,若是非內聯的情況呢?
我們可以發現,a.o就會產生A::max()的函數符號,這樣的話,若連結器去連結main.o a.o :
我們就會發現main.o 帶U標記的undefined的A::max()被尋找到了,並在最後的.o中進行了填充。
那麼,為什麼inline的函數定義放在標頭檔就可以呢?因為我們使用的時候,會#include "a.h",那麼這個時候編譯器首先會預先處理展開,這樣也就包括了A::max()的定義,使用g++ -E http://a.cc > a.i; vim a.i 後如所示:
那麼這個時候,http://main.cc在本源檔案就可以找到A::max()的定義了,然後讓我們看最後的符號表:
也的確如我們意想的不是U的undefined。
那麼,這裡多提一句的是,在現代的C++編譯器中,我們幾乎都做一個最佳化叫做內嵌函式最佳化,因為我們在最佳化的時候,若不是IPA這樣的都是以一個函數來進行,那麼我們內嵌函式最佳化後就會把相關的定義展開在一個函數中,這樣就可以進行更好的最佳化。而這個內嵌函式的最佳化層級,在每一個編譯器中是不同的,但是在我們實際C++編譯器的實現中,其實已經完全不是很多教科書提到的幾行了,我們往往是100行,乃至更多行的函數都會內聯展開,就是為了更好的最佳化。而我們內聯最佳化的話,其實也就是根據你的函數是多少行來進行決定的,於是在現代C++編譯器中,如果你是為了inline最佳化而加上inline的話,我認為inline的意義在這裡已經完全不需要了,現代的C++編譯器最佳化已經非常強悍了。所以,若將來inline被C++廢棄,也是完全可以理解的。
constexpr函數------c++ primer