宏病毒專殺cleanmacro(在C 中使用宏的一些實用經驗總結)
一個眾所周知的事實是-宏是很糟糕的,宏是一個歷史的遺留的產物,已經無法很好的適應現代c 的發展。當然,有也一些宏是也是很不錯的。
每條規則的背后都是有例外的,所以不要輕易的說“禁止使用宏”,雖然有一些宏可能會另代碼看起來很不舒服(讓人困惑),但還是有一些宏可以讓顯著地提升代碼的可讀性和正確性。
一個很糟糕的宏: max
宏有很多缺點,其中一個是宏是沒有作用域的,這也就是如果一個文件中定義了一個宏,如header.hpp中場景了一個#define指令,那么該文件后續所有行的代碼都會受到該宏的影響,直接或間接include該頭文件的文件也一樣。
void innerFunc(){#define MACRO_IN_FUNCTION 1 }// 我們可以在超出函數作用域的地方繼續使用該宏#ifdef MACRO_IN_FUNCTION// TODO something.#endif
如在上面的代碼中我們在函數內部分定義了一下宏MACRO_IN_FUNCTION,那么這函數定義之后的所有代碼中都可以使用該宏,即是超出了函數的作有域,也就是說你不能將宏限制在一個函數,namesapce,或者類中。
考慮下面一個例子
#define max(a,b) (a < b) ? b : aint x = 42;int y = 43;int z = max(x, y); std::cout << x << 'n' << y << 'n' << z << 'n';
這個代碼的輸出是多少呢?毫無疑問,輸出應該是:
424343
好的,下面我們來稍微改變一下我們的代碼
int x = 42;int y = 43;int z = max( x, y);std::cout << x << 'n' << y << 'n' << z << 'n';
從語法上來講,就是一段合理的代碼,在語義上我們期望的結果應當是x是43,y和z是44,然而我們得到的結果是:
434545
為什么會是這個結果呢?當考慮到宏所做的事情后,這個結果卻是正確的:宏只是簡單的進行文本替換,如何讓查看編譯器進行宏展開后的結果呢,本人使用g ,只需要在g 命令中添加‘-E’即可,即g -E *.cpp,下面就是宏展開后的代碼:
int x = 42;int y = 43;int z = ( x < y) ? y : x;std::cout << x << 'n' << y << 'n' << z << 'n';
從展開后的結果中可以看出,最大的值y,有兩次的自增( )操作。基于文本的替換的宏在與C 進行結合時,可能會產生非常危險的混合,例如,如果你在其它文件中定義了函數max,然而你是無法調用到的,預處理器(preprocessor)會首先將其按宏的方式進行展開。
除此之外,宏還有很多其它問題,例如無法進行斷點調試等等。雖然宏有很多問題,但在很多情況下宏卻可以用來提升代碼的質量。
1. 用于連接兩個C 特性
C 具有非常豐富的特性,但是在一些高級的設計,多個部分之間無法做到無縫連接。
例如,我們需要在庫中實現Accept并將此函數注入到應用程序的DocElement層次結構中。可惜,c 沒有這樣的直接機制。也有使用虛擬繼承的變通方法,但虛函數的調用具有一定的性能開銷。我們可以定義一個宏,并要求visitable層次結構中的每個類在類定義中使用該宏。但是,當我們在代碼中使用宏時,需要保持代碼的可控性,Andrei Alexandrescu提供了一些指導意見-定義宏的一個最重要的規則是讓它盡可能少地自己執行,并盡可能快地將其轉發給“真正的”實體(函數、類),也即是宏的邏輯足夠簡單。
#define DEFINE_VISITABLE() virtual ReturnType Accept(BaseVisitor& guest) { return AcceptImpl(*this, guest); }
2. 利用宏來減少冗余
在寫代碼時,如果一段相同的代碼需要鍵入多次,那么使用宏可以減少這種冗余,并讓代碼看起來足夠賞心悅目。
在現代C 中我們可能需要經常用到std::forward來傳遞左值或右值引用:
#define DEFINE_VISITABLE() virtual ReturnType Accept(BaseVisitor& guest) { return AcceptImpl(*this, guest); }
此模板代碼中的&&表示值可以是l-value或r-value引用,具體取決于它們綁定的值是l-value還是r-value。std:forward允許將此信息傳遞給g。
但這需要很多代碼來表達,每次輸入都很麻煩,而且在閱讀時還會占用一些空間。所以,為了簡潔起見,我們可以定義一個宏來完成這個事情:
#define FWD(...) ::std::forward(__VA_ARGS__)
這樣我們的代碼看起來就是這要的:
templatevoid f(MyType&& myValue, MyOtherType&& myOtherValue){ g(FWD(myValue), FWD(myOtherValue));}
顯然,這段代碼比原始代碼更加直觀,更具有可讀性。
3 宏可以帶來更低層級的多態機制
宏可以被用于多態,但這只是多態的一個特殊情況,需要在編譯階段前被resolved,也就是發生在預處理階段。
這是怎么做到的呢?您可以定義以-D開頭的編譯參數,并且可以使用代碼中的#ifdef指令測試這些參數的存在性。根據它們的存在,您可以使用不同的#define來為代碼中的表達式賦予不同的含義。
至少有兩種信息你可以通過這種方式傳遞給你的程序:
允許系統調用代碼可移植的操作系統類型(UNIX vs Windows)
可用的c 版本(c 98、c 03、c 11、c 14、c 17等)。
在設計用于不同項目的庫代碼中,讓代碼知道c 的版本是很有用的。它為庫代碼提供了靈活性,使其能夠在可用的情況下編寫高效的代碼實現。
在使用c 高級特性的庫中,如果庫必須處理某些編譯器bug,那么傳遞有關編譯器本身及其版本的信息也是有意義的。這是Boost中的一個常見實踐方式。
無論哪種方式,對于與環境或語言相關的指令,您都希望將這種檢查保持在盡可能低的級別,并將其深深封裝在實現代碼中。
總結
在C 代碼中爛用宏是一個很evil的事情,理解宏的機制,合理適合宏往往能帶來非常大的收益,例如本人最近的工作中,需要將C 對象導致到js層,使用宏就大大提升了工作效率。