在C++中使用宏的一些實用經驗總結

在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 : a
int x = 42;
int y = 43;
int z = max(x, y);

std::cout << x << '\\n'
<< y << '\\n'
<< z << '\\n';

這個代碼的輸出是多少呢?毫無疑問,輸出應該是:

42
43
43

好的,下面我們來稍微改變一下我們的代碼

int x = 42;
int y = 43;
int z = max(++x, ++y);
std::cout << x << '\\n'
<< y << '\\n'
<< z << '\\n';

從語法上來講,就是一段合理的代碼,在語義上我們期望的結果應當是x是43,y和z是44,然而我們得到的結果是:

43
45
45

為什麼會是這個結果呢?當考慮到宏所做的事情後,這個結果卻是正確的:宏只是簡單的進行文本替換,如何讓查看編譯器進行宏展開後的結果呢,本人使用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__)

這樣我們的代碼看起來就是這要的:

template
void 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層,使用宏就大大提升了工作效率。


分享到:


相關文章: