幾天前我發表了一篇關於 C 語言可變參數處理的博文,本意是分享用函數型宏處理變參的技巧,沒想到卻因為宏的封裝方式引發了激烈討論。許多資深工程師關注的焦點不是變參處理本身,而是為什麼我沒用業界常見的 do{}while(0) 來包裹宏,而是直接用大括號 {}。
這讓我想起《0bug-C/C++商用工程之道》出版時的類似經歷:部分讀者不是為了學習書中的核心思想,而是專門找錯別字、找技術細節的「錯誤」來彰顯自己水平。這種「買櫝還珠」的閱讀方式,實際上阻礙了真正的技術成長。
在原文中,我定義了這樣的宏:
#define TONY_FORMAT(nPrintLength, szBuf, nBufferSize, szFormat) \
{ \
va_list pArgList; \
va_start(pArgList, szFormat); \
nPrintLength += Linux_Win_vsnprintf(szBuf + nPrintLength, \
nBufferSize - nPrintLength, szFormat, pArgList); \
va_end(pArgList); \
if (nPrintLength > (nBufferSize - 1)) nPrintLength = nBufferSize - 1; \
*(szBuf + nPrintLength) = '\0'; \
}
因為沒用 do{}while(0),在 if-else 中調用時必須顯式加大括號:
if (!bWithTimestamp) {
TONY_FORMAT(nLength, szBuf, TONY_LINE_MAX, szFormat);
} else {
TONY_FORMAT_WITH_TIMESTAMP(nLength, szBuf, TONY_LINE_MAX, szFormat);
}
當然,也可以通過去掉調用後的分號來避免加大括號,但這違背了常規編碼習慣。
我堅持不使用 do{}while(0) 的根本原因在於語義清晰度與團隊規範:
do-while 的設計初衷是循環,而非宏包裹do{}while(0) 字面語義是「循環執行零次」,但實際卻執行一次。這種「字面與行為相反」的特性極易誤導初學者:
int i = 0;
do {
i++;
} while (0); // i 變成 1,而非 0
更令人困惑的是:while(0) 執行一次,while(1) 卻是死循環。這種反直覺的語義違背了「代碼即文檔」的原則。
C 標準從未規定 do-while 用於宏封裝。將循環語句強行用作宏包裹符,屬於「掛羊頭賣狗肉」,增加了代碼的認知負荷。
我的選擇基於《0bug》書中提倡的語言裁剪使用原則:在多種解法中,選擇「最簡單、最不易出錯、最易閱讀」的一種並形成團隊標準。雖然用大括號要求調用處加 {},但這符合直覺,且編譯器會強制檢查結構匹配,反而減少了隱性錯誤。
這套規範在我的團隊執行超過 15 年,2008 年帶領團隊開發商用服務器集群(十幾萬行 C/C++ 代碼),僅 51 個 Bug,核心層僅 7 個。規範化看似限制了靈活性,實則保障了交付質量與團隊效率。
我寫代碼不是為了炫耀技巧,而是為了簡單、直接地交付無 Bug 的產品,讓團隊賺到錢。這種工程化思維,才是我希望傳遞的核心價值。