在C/C++項目中合理使用宏詳解
C++項目中常使用宏來做跨平臺、功能實現(xiàn)隔離、變量定義的功能,這篇文章來討論下是否所有情況下都適合用宏
小D的故事
程序員小D接到一個任務,需要給同事A提供一個復雜公式的實現(xiàn)。輸入為一組參數(shù),輸出一個計算結果。
大致如下:
double computeSomeThing(double paramA,double paramB,double paramC);
小D很快完成了。過了幾天同事A又來找他,說現(xiàn)在需要提升該函數(shù)的性能,建議改為在float類型上,用一些SIMD指令。且同事A表示不是很愿意修改接口。于是小D在考慮以下兩點后決定用一個宏把原來double的實現(xiàn)和float的實現(xiàn)分開來。
1、上層需求變動性比較大,說不定哪天又要用double了。所以還是保留double類型的實現(xiàn)
2、用宏把兩份代碼隔開來,互相不影響比較省事
于是代碼就變成了這樣:
double computeSomeThing(double paramA,double paramB,double paramC) { #ifdef _USE_DOUBLE_ // do something in double #else // convert dobule to float // do something in float // convert float to double #endif }
同事A很滿意,因為他只要替換一下.so或.a即可,代碼層不需要改動。于是和小D合作開發(fā)了很多這樣的函數(shù),并且都有float和double兩種實現(xiàn)。在對性能要求高的時候要求小D提供float版本;性能要求低,精度要求的時候要求小D提供double版本。
此時小D會在出庫的時候感到一絲不方便。第一,版本號中需要區(qū)分float和double版本。
第二,因為用宏隔開,切換兩個版本的時候需要重新編譯,而代碼量很多所以編譯時間很長,但這些都是能克服的。
直到有一天同事小B的模塊也需要這個庫,并且小A和小B的模塊要組合起來給小C用,最要命的是小A和小B的模塊分別要用float版本和double版本。所以此時應該提供float版本so還是提供double版本so呢。
問題分析
在上面的場景中,小D作為一個基礎庫的提供者不應該因為同事不愿意修改接口或者圖方便用宏去隔離功能,使得一個接口有了二義性。比較合適的一種做法是,再提供一個控制選擇變量,來選擇用哪種實現(xiàn),即允許運行時決定用float還是double版本。
double computeSomeThing(double paramA,double paramB,double paramC,bool isFast);
或者小A就是不愿意改接口(考慮實際項目中,接口參數(shù)復雜且調用分散在各處),那么也可以通過增加接口實現(xiàn)。
double computeSomeThing(double paramA,double paramB,double paramC);
void setFast(bool isFast);
下面的情況用宏做隔離就是比較合理的選擇。
比如一套代碼要分別運行在linux和windows上,依賴的頭文件、部分基礎函數(shù)接口都是有區(qū)別的。此時用宏去隔離就比較合理。因為這兩個版本在運行時永遠不會同時出現(xiàn)。除了平臺差異性外,版本管理也可以用宏來做隔離。
比如opencl 1.2和opencl 2.0版本相比較的話,2.0版本中新增了SVM相關的接口。當一個opencl程序未來可能運行在1.2版本的設備和2.0版本的設備上時。
可以用宏來選擇是否屏蔽掉SVM接口。因為2.0的接口運行在1.2的設備上時,無法從環(huán)境中獲取2.0新增的接口實現(xiàn)導致程序跑不起來(1.2的相關so中沒有SVM函數(shù)實現(xiàn))。
不過這個問題用宏來處理也不是最優(yōu)的,使用dlopen可以有更靈活的實現(xiàn)。
總結
對于做基礎庫提供給很多人使用的同學,當用宏隔開的代碼有可能會同時運行在一個環(huán)境時建議改為運行時選擇走哪條分支。但肯定互相不兼容的時候就放心的用宏吧,比如跨操作系統(tǒng)。
另外提一下,對于有很多代碼的大項目用宏的時候也要慎重考慮一下,不要動不動就用宏去做一些功能開關,因為編譯時間太長是很影響效率的。
比如有以下宏定義:
#define _OPEN_LOG_ #ifdef _OPEN_LOG_ #define LOG_PRINT(...) printf(...) #else #define LOG_PRINT(...) #endif
開發(fā)階段代碼中到處插著LOG_PRINT的使用,發(fā)布時關閉打印又是一波整個項目重新編譯。再多來幾個這種功能,每次切換又是整個項目重新編譯,非常煩人??梢杂煤瘮?shù)指針代替:
typedef void (*LogPrint)(const char * pstrMsg); LogPrint g_LogPrint; void LogPrint_Imp(const char *pstrMsg) { printf("%s\n",pstrMsg); return; } void LogPrint_Empty(const char *pstrMsg) { return; } int main(int argc,char **argv) { // 此處對日志功能進行開關 g_LogPrint = LogPrint_Imp ; //g_LogPrint = LogPrint_Empty ; // ..... } void someFun() { g_LogPrint("in someFun"); //到底打印還是不打印,運行時決定 }
在這個例子中,關閉日志時編譯器只會對main函數(shù)所在的文件進行重新編譯,就不用費時費力的重新編譯整個項目了。而且還可以把g_LogPrint的賦值的行為通過接口開放到上層,由調用者決定是否需要打開log。
再舉個例子,有些人喜歡項目中各個代碼模塊中用到的參數(shù)提到一個頭文件中,然后各個.c都包含這個頭文件。就像這樣:
// GobalParam.h #ifndef XX_XX #define XX_XX #define DETECTION_MAX 100 #define INPUT_WIDTH_MAX 4096 #define INPUT_HEIGHT_MAX 4096 // 諸如此類很多宏 #endif
我個人覺得下面這種實現(xiàn)更好
// GobalParam.h #ifndef XX_XX #define XX_XX extern const int DETECTION_MAX; extern const int INPUT_WIDTH_MAX ; extern const int INPUT_HEIGHT_MAX ; // 在某個.c或.cpp中賦值 :const int INPUT_HEIGHT_MAX = 100; #endif
這樣你對某個參數(shù)修改的時候,就不用眼巴巴的等著所有包含此頭文件的編譯模塊重新編譯了。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。如有錯誤或未考慮完全的地方歡迎留言討論,望不吝賜教。
相關文章
C語言中getchar函數(shù)詳解看這一篇就夠了(函數(shù)功能、使用、返回值)
getchar讀取字符的函數(shù),今天通過本文給大家介紹C語言中getchar函數(shù)簡介用法示例詳解,感興趣的朋友跟隨小編一起看看吧2023-02-02詳解C++中十六進制字符串轉數(shù)字(數(shù)值)
這篇文章主要介紹了詳解C++中十六進制字符串轉數(shù)字(數(shù)值)的相關資料,這里提供兩種實現(xiàn)方法,需要的朋友可以參考下2017-08-08C++智能指針shared_ptr與weak_ptr的實現(xiàn)分析
shared_ptr是一個標準的共享所有權的智能指針,允許多個指針指向同一個對象,定義在 memory 文件中,命名空間為 std,這篇文章主要介紹了C++ 中 shared_ptr weak_ptr,需要的朋友可以參考下2022-09-09