標籤:

如何看待C++前置聲明?

最新的Google C++ Style Guide對前置聲明的建議是:Avoid using forward declarations where possible. Just #include the headers you need.

Google C++ Style Guide

然而在一些其他材料中,則鼓勵程序員儘可能的使用前置聲明,例如Mozilla Coding Style中對前置聲明的建議是:Forward-declare classes in your header files instead of including them, whenever possible. For example, if you have an interface with a void DoSomething(nsIContent* aContent) function, forward-declare with class nsIContent; instead of #include "nsIContent.h"

Coding style

其餘大部分材料也都是鼓勵使用C++前置聲明,甚至在舊版本的Google C++ Style Guide中,對C++前置聲明也是持鼓勵態度。

所以我們應該如何看待C++前置聲明?Google C++ Style Guide對C++前置聲明的態度為什麼會發生如此顯著的變化?


利益相關:在Google寫了4年C++。

我是經歷了Google內部從『傾向於使用前置聲明』到『傾向於使用#include』的這個過程的。事實上在很多年前Google內部就開始了對這兩者的比較和探討。在2014年,內部有一篇總結性文章指出了前置聲明將造成的十種危險。最終C++ Code Style經歷了一個過渡期之後全面倒向了#include。

簡單來說,前置聲明最大的好處就是『節省編譯時間』。畢竟C++的編譯時間長已經是一個臭名昭著人人喊打的問題。但對於Google來說,這方面的效率節省就不見得那麼可觀了——畢竟Google內部有超大規模的分散式編譯集群『Forge』。哪怕是十萬以上的target,全部build一遍也就是幾分鐘的事情。

與此同時,前置聲明帶來的問題則顯得更加關鍵:

例如,如果一個類的實現者需要把這個類改個名字/換個命名空間,出於兼容性他原本可以在原命名空間里/用原名通過using來起一個別名指向新類。然而別名不能被前向聲明。內網有一份代碼改動一下子試圖修改總計265個頭文件,就是實現者為了要改這個類的名字而不得不去改所有的調用處。想一想,如果這265個文件分屬於50個不同的團隊,你得拿到50個人的同意才能提交這份改動,想不想打人?

再舉一個code style中提到的,更為嚴重的例子——它可能導致運行時出現錯誤的結果

// b.h:
struct B {};
struct D : B {};

// good_user.cc:
#include "b.h"
void f(B*);
void f(void*);
void test(D* x) { f(x); } // calls f(B*)

若把#include換成前置聲明,由於聲明時不知道D是B的子類,test()中f(x)就會導致f(void*)被調用,而不是f(B*)。

再比如,C++標準5.3.5/5中規定,delete一個不完整類型的指針時,如果這個類型有non-trivial的析構函數,那麼這種行為是未定義的。把前置聲明換成#include則能保證消除這種風險。

誠然,從理論上說,一個牛逼的程序員當然是可以通過分析一個頭文件的源碼來決定會不會碰到以上諸多坑,並由此決定用哪個好。但一來不是所有人都是牛逼程序員,二來,把精力花費在這件事情上真的值得嗎?


從我的經驗來看,C++ 編譯速度慢似乎 #include 只佔了很小部分,因為同樣是 #include,純 C 代碼就可以編譯得很快。不使用 STL、不使用模板的 C++ 代碼也可以編譯得很快,而如果使用了模板,很明顯編譯速度就會降下來。模板展開應該是最主要花時間的過程。


前置聲明縮小編譯時間實際上只是一個正確的謊言。因為花費同樣的智力,前置聲明比#include的確是編譯起來快了一點,但是這是建立在犧牲廣大維護者的基礎上的。微軟的Office最近一年完成了一個把集群編譯時間從20+個小時縮短到5個小時的工作,沒有加換機器,純粹靠跟前置聲明無關的其他方法來完成。可見只要你願意,#include也可以編譯得快。

現在我們沒事就瞎** #include,反正什麼都不用擔心,也不會因為前輩們為了屁大點好處讓我們#include的時候出翔導致加班。

如果連最後一點縮短編譯時間的好處都沒有了的話,那#include就是全面優於前置聲明的,除非在極少情況下你需要寫出循環引用的東西——這不可避免,但是反正這個聲明的實現也是在同一個文件里完成的,所以不會造成各種傻逼事情的發生。


該用void*的時候請老實用void*。明明只是個佔位的指針,為了那點語法糖製造出樓上的各種問題真心不值得。

如果不是佔位用的指針,那麼用前向聲明是不應該的,別圖省事,把循環依賴破了吧。


「前向聲明」無非就是C遺留的編譯模型不能夠自動幫你推導類型信息,所以需要手動添加一條元數據而已。你看其他大部分語言,能夠自動分析模塊/程序集來解決這問題。

至於這樣做的好處,我傾向於培神的觀點,不會把一坨坨屎都丟在頭文件里,畢竟頭文件是你暴露出去的介面。


類作者應該提供前置聲明的頭文件,類似於iosfwd。類的使用者不能,有時也不可能,聲明正確的前置聲明。

如果沒有提供,那就直接包含對應類的頭文件,這類本身可能內聯函數多,或已經使用了piml成例


前置聲明隱藏依賴啊親們,扯什麼編譯時間啊。

沒聽說過 pImpl?

不用前置聲明難道直接把 Windows.h 那一坨屎扔在頭文件里讓別人吃屎么……

再比如,C++標準5.3.5/5中規定,delete一個不完整類型的指針時,如果這個類型有non-trivial的析構函數,那麼這種行為是未定義的。把前置聲明換成#include則能保證消除這種風險。

為什麼要 delete?

另外知乎這個破編輯器做不明白了么。。。Edge 下根本沒法正常用


僅當兩個類相互引用時用前置聲明。


建議僅將前置聲明用於解決循環引用問題。

比如A包含一個指向B的指針,B包含A的一個實例,這種情況下,使用前置聲明。


當兩個類互相用到對方的時候,前置聲明類,就很重要了


google c++ style guide還說不要使用異常。好處和壞處都說明了,按實際情況分析要不要用不就好了。

其實這類問題,怎麼做差別都不大,真正危險的是頭文件亂包頭文件,最後隨便編一個cpp,都間接地把全工程的頭文件包齊了,開預編譯頭都救不了。


說簡單點,就是盡量避免循環依賴


那麼多提到循環引用的,是不是考慮你要提取代碼進行降級處理了


前置聲明,兩個類相互依賴時候,以及減少頭文件的包含層次,減少出錯。


推薦閱讀:

C++中改變了const變數的值,但輸出卻沒有改變,而內存中數據的確變了,這是怎麼回事?
如何才能自學編程並學好編程?
系統調用真正的效率瓶頸在哪裡?
C語言中,scanf("%d", &a),在設計這個語法結構時為什麼要有地址符&,沒有行不行?
有沒有求多邊形的狹窄部分的演算法?

TAG:編程 | C |