警告“未引用的形參/區域性變數”的消除方法
如果我們編譯以下程式碼:
#include <stdio.h>
int main(int argc, char** argv)
{
int n;
int nRet = printf("Hello, world!");
return 0;
}
編譯器一般會發出以下警告(VS2015):
1>f:\mycode\cpptest\main.cpp(55): warning C4100: “argv”: 未引用的形參
1>f:\mycode\cpptest\main.cpp(55): warning C4100: “argc”: 未引用的形參
1 >f:\mycode\cpptest\main.cpp(67): warning C4101: “a”: 未引用的區域性變數
1>f:\mycode\cpptest\main.cpp(68): warning C4189: “nRet”: 區域性變數已初始化但不引用
編譯器認為,既然我們已經宣告/定義了某變數,那我們就有使用它的意圖。所以當它檢測到我們未對此變數進行實際的使用時,就會發出警告,提醒我們檢查程式碼是否存在錯誤。
為了程式碼編譯時輸出視窗的整潔(以使有價值的提示資訊更醒目),我們通常會消除掉此類警告(當然是在我們確認這是有意為之之後)。而在消除此類警告時,隱式地,我們一般有以下四種期望:
- 編譯器不再發出此警告;
- 不會引起程式碼邏輯變化;
- 不會造成效能損失;
- 不會對程式碼維護造成負擔,包括閱讀和修改;
以下提供兩種經典解決方案,並分析網上流傳的一些方法為什麼不可取。
一、巨集UNREFERENCED_PARAMETER
Windows開發人員使用巨集定義方案。在Windows平臺下,可以在<winnt.h>
中找到以下巨集定義:
#define UNREFERENCED_PARAMETER(P) (P)
使用方法如下:
int main(int argc, char** argv)
{
UNREFERENCED_PARAMETER(argc); // 手動醒目
UNREFERENCED_PARAMETER(argv); // 手動醒目
int n;
UNREFERENCED_PARAMETER(n); // 手動醒目
int nRet = printf("Hello, world!");
UNREFERENCED_PARAMETER(nRet); // 手動醒目
return 0;
}
因為實際上,程式碼被巨集展開為如下形式:
int main(int argc, char** argv)
{
(argc); // 手動醒目
(argv); // 手動醒目
int n;
(n); // 手動醒目
int nRet = printf("Hello, world!");
(nRet); // 手動醒目
return 0;
}
,即argc
、argv
、n
和nRet
確實被使用(作為一條單獨的語句),所以警告不會再產生。
如果嚴格按照這樣一個變數一條語句的寫法,程式碼邏輯就絕不會發生變化;同時,在release模式下,這些程式碼完全會被優化掉,所以對效能也不會造成影響。
另外,巨集的名稱UNREFERENCED_PARAMETER
已經說明,這是一個未被引用的引數,所以在閱讀時不僅不會造成障礙,反而會有助於理解;而在程式碼修改時,如果真正使用了些變數,直接將此句刪除,也是很簡單的一件事。
但是,實際上在Windows中,此巨集僅被用來消除warning C4100: 未引用的形參
警告,因為巨集的名稱是UNREFERENCED_ PARAMETER。所以如果在我們的專案中應用此解決方案時,最好自己定義一個名稱更應景的巨集,譬如:
#define UNREFERENCED_VALUE(P) (P)
以同時完美應用於上述三種警告。
二、模板空函式PX_UNUSED()
NVIDIA的PhysX
專案開發人員使用模板空函式解決方案。在PhysX
開原始碼的include\foundation\PxPreprocessor.h
中可以找到如下定義(同時還有某些對此方法的疑問,下面各節將會回答這些疑問):
// avoid unreferenced parameter warning (why not just disable it?)
// PT: or why not just omit the parameter's name from the declaration????
template <class T> PX_CUDA_CALLABLE PX_INLINE void PX_UNUSED(T const&) {}
使用方法如下:
int main(int argc, char** argv)
{
PX_UNUSED(argc); // 手動醒目
PX_UNUSED(argv); // 手動醒目
int n;
PX_UNUSED(n); // 手動醒目
int nRet = printf("Hello, world!");
PX_UNUSED(nRet); // 手動醒目
return 0;
}
首先,原本未被引用的變數確實被用來呼叫函數了,所以警告被消除。應該注意到,在PX_UNUSED()
實現時有一個細節,它沒有給出引數的具體名稱const T&
,所以警告不會再傳遞(見“三、為什麼不應該直接刪除函式引數名”)。
其次,因為僅被用來呼叫空函式,且是傳引用方法,所以程式碼邏輯不會被影響;而空函式在release模式下也會被優化掉,所以也不會產生效能影響,頂多在編譯時生成模板函式增加了一點點時間。
最後,此函式被使用時,同樣醒目且易刪除,所以也不會對程式碼維護造成額外的負擔。
相對於巨集定義的方式,我比較贊成此種解決方案,因為前者有可能因為誤用而造成莫名其妙的錯誤。
三、為什麼不應該直接刪除函式引數名
首先,此種方法只能解決警告“warning C4100: 未引用的形參“,而對其他兩種警告無能為力,因為有的時候未引用的變數確實是不方便刪除的。例如以下程式碼在release下會引發警告,而我們又需要在deubg模式下保留它:
bool ok = doSth(...);
ASSERT(ok);
另一方面,直接刪除函式引數名會對程式碼維護造成負擔。在閱讀一個.cpp檔案中的函式時,我們通常期望通過引數的名稱捕捉到關於函式功能或實現方式的線索,而且這種期望是下意識的,是一種連續的”流“。如果突然某個函式缺失了引數名稱,不僅導致函式的線索的消失,同時也會打斷這種下意識”流“,打斷閱讀程式碼的狀態。同時,此時程式碼閱讀者會下意識地跳轉到標頭檔案來檢視函式的宣告,以期望找回”丟失的引數名“,這又浪費了額外的時間和精力。而且,在修改程式碼時,如果突然此變數又被引用,那麼維護人員將又不得不將從標頭檔案中找回引數名,並加回.cpp檔案中,對比簡單地刪除,這種操作較為煩瑣。所以,此種消除警告的方法應該是禁止的。
但是有一個例外,就是如”二、模板空函式PX_UNUSED()“所示的函式PX_UNUSED()
,此函式刻意地刪除引數名以實現其目的。
四、為什麼不應該直接註釋函式引數名
參考上一節”三、為什麼不應該直接刪除函式引數名“,首先此方法僅對一種警告起作用。
其次是習慣問題。大多數程式設計師的習慣,是在函式實現檔案是註釋掉函式引數的預設值:
// .h檔案中,函式的宣告
void doSth(int n = 0);
// .cpp檔案中,函式的實現
void doSth(int n/* = 0*/)
{
...
}
如果突然出現這樣的程式碼:
void doOth(int/* m*/)
{
...
}
也會對程式碼閱讀產生輕微的障礙。如果程式碼是小範圍傳播,倒也沒有問題;如果是大型,由多人蔘與的專案,則應當照顧的大多數人的習慣。所以,此種方法也不建議使用。
五、為什麼不應該直接抑制此類警告
有人提議,使用預處理指令#pragma warning(disable:4100)
抑制此類警告。此種方法應該是嚴格禁止的!
編譯器的警告並不是無的放矢,有時候,確實可能由於變數名拼寫錯誤從而觸發此警告,如下例子:
int sum(int n1, int n2, int n3)
{
return n1 + n2 + n2;
}
如果抑制了此警告,那你就失去了在BUG萌芽時就將其消滅的機會。後面,你有可能要花數個小時去尋找這個BUG!
從另一個角度講,這也反映了一個人對待程式碼的態度。如果他對編譯器發出的警告僅感到厭倦從而抑制了之,而不是去確認問題確是否真的存在,或尋找一種更無害的消除方式,那麼他將不能成為一個好的程式設計師。從我個人角度來講,我不願意與這樣的人共事。