我們在編寫C++類庫時,為了隱藏實現,往往只能忍痛舍棄模版的強大特性。但如果我們只需要有限的幾個類型的模版實現,並且不允許用戶傳入其他類型時,我們就可以將實例化的代碼放在cpp文件中實現了。然而,當我們又需要針對特定類型進行模版偏特化時,由於gcc編譯器不允許直接在類中進行偏特化聲明,所以正確的寫法變得比較復雜。本文通過一個簡單的求log2函數的例子,提供了一個在cpp中同時進行偏特化和實例化的一般寫法,並且可以使用static_assert在編譯期檢查參數的實現。
現在假設我們有一個叫做"Math"的工具類,它的所有操作都以public靜態函數提供。現在我們要添加一個log2函數,並且這個函數的有兩個版本:
int log2(float value); float log2(float value);
我們知道在C++中函數重載必須要有不同的參數列表,直接這樣聲明肯定是不行的。但我們又不想在函數名中加上返回類型,此時我們很自然地想到了使用模版。這樣我們期望的使用方法如下:
int resultInt = Math::log2<int>(1000.f); float resultFloat = Math::log2<float>(1000.f);
(PS. 這個例子僅作講解之用,至於是否要做成模版參數的樣式仍需按照項目的規范來設計。)
下面我們先給log2函數附上一個不正確的實現,讓我們的例子能夠編譯並運轉起來。
頭文件Math.h中的代碼如下:
class Math { public: template <class _Type> static _Type log2(float value) { return 0; } };
在main.cpp中,加入我們的測試代碼:
int main() { int resultInt = Math::log2<int>(1000.f); float resultFloat = Math::log2<float>(1000.f); printf("resultInt = %d\n", resultInt); printf("resultFloat = %f\n", resultFloat); return 0; }
編譯並運行,可以看到控制台上輸出的resultInt和resultFloat都是0。
現在這個庫函數可以使用了。但所有人都可以在math.h中查看到我們所有的實現代碼,如果我們想要隱蔽實現,就需要想辦法把它們放到cpp文件中。
我們注意到log2函數的返回值只能是int或float,返回其他自定義類型都是沒有意義的,因此我們不必讓所有編譯單元都看到函數的實現體,只需要在cpp中定義一個實現,再在實現體之后顯示裝載模版類即可。
現在創建一個math.cpp,把我們代碼的實現部分移過去。
#include "Math.h" template <class _Type> _Type Math::log2(float value) { return 0; } template int Math::log2<int>(float value); template float Math::log2<float>(float value);
編譯並運行,我們仍得到了方才一致的結果。
將實現放在cpp中不僅起到了隱蔽實現的功能,同時還獲得了一個額外的好處,就是一旦用戶使用了錯誤的類型實例化,就會引發鏈接錯誤。
倘若我們在main函數中添加一句:
std::string resultString = Math::log2<std::string>(1000.f);
再次編譯,我們會看到鏈接器報錯:
無法解析的外部符號 "public: static class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > __cdecl Math::log2<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > >(float)" (??$log2@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Math@@SA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@M@Z)
(VS2013)
把這一句注釋掉(下面還會用到),我們開始為log2添加正確的實現。
先訪問 http://www.musicdsp.org/showone.php?id=91 這里有一個非常快速的log2算法(當然不是十分精確),但是想要直接使用這些代碼時我們卻遇到了難題:這個算法返回int的函數floorOfLn2和返回float的函數approxLn2是完全不同的兩個實現,然而我們的模版函數只有一個!要解決這個問題,需要針對int和float類型編寫偏特化實現。
這里有一個值得注意的地方:我們在Visual Studio編寫模版偏特化時,可以直接將偏特化聲明直接放在類的定義部分。然而這在gcc中是不能通過編譯的。想要我們的代碼跨平台,就要遵循標准寫法,將偏特化實現放在類外。我們的例子實現的代碼已經在cpp中了,因此我們可以直接在cpp中添加偏特化的代碼。
修改后的math.cpp如下:
#include "Math.h" template <class _Type> _Type Math::log2(float value) { return 0; } template <> int Math::log2<int>(float value) { assert(value > 0.); assert(sizeof(value) == sizeof(int)); assert(sizeof(value) == 4); return (((*(int *)&value)&0x7f800000)>>23)-0x7f; } template <> float Math::log2<float>(float value) { assert(value > 0.); assert(sizeof(value) == sizeof(int)); assert(sizeof(value) == 4); int i = (*(int *)&value); return (((i&0x7f800000)>>23)-0x7f)+(i&0x007fffff)/(float)0x800000; } template int Math::log2<int>(float value); template float Math::log2<float>(float value);
編譯運行,我們看到程序已經輸出了正確的結果:
resultInt = 9
resultFloat = 9.953125
另外我們注意到,模版函數的那個一般實現:
template <class _Type> _Type Math::log2(float value) { return 0; }
已經不再使用了,我們可以將它刪除,完全不會造成任何影響。
現在一個接近完美的log2函數就已經制作完畢了。
那么它的不足之處在哪里?我們再回過頭觀察math.h,發現函數的聲明只有一個光禿禿的 template <class _Type> static _Type log2(float value)。對於用戶來說,僅看到這一個頭文件,是完全想象不到函數還有一個返回值的限定的。如果他不小心使用了錯誤的類型,IDE只會給出一個極不友好的鏈接錯誤,這樣用戶肯定就抓狂了。
想要提供一個友好的錯誤提示,可以考慮使用C++11引入的新特性static_assert,它可以幫我們檢測出錯誤的類型,並在編譯時就發現它們。
在編譯期判斷類型是否相同,我們需要引入一些type_traits。
定義如下:
struct TrueType { static const bool value = true; }; struct FalseType { static const bool value = false; }; template <class _A, class _B> struct IsSameType : FalseType { }; template <class _A> struct IsSameType<_A, _A> : TrueType { };
接下來我們將原來的log函數包裝起來,放在private修飾符下,並改名為_log (不要忘記同時修改math.cpp中的符號)。
再新建一個一模一樣的log函數,我們要做的就是在這個函數中寫入static_assert檢查模版參數類型,如果類型無誤,我們才會調用真正的_log2函數。
修改后的math.h如下:
struct TrueType { static const bool value = true; }; struct FalseType { static const bool value = false; }; template <class _A, class _B> struct IsSameType : FalseType { }; template <class _A> struct IsSameType<_A, _A> : TrueType { }; class Math { public: template <class _Type> static _Type log2(float value) { static_assert(IsSameType<_Type, int>::value || IsSameType<_Type, float>::value, "template argument must be int or float"); return _log2<_Type>(value); } private: template <class _Type> static _Type _log2(float value); };
把之前注釋掉的傳入std::string類型的那條語句恢復,再次編譯,我們會看到這次報的是編譯錯誤了,內容就是我們在static_assert中填寫的內容。
本文由 哈薩雅琪 原創,轉載請注明出處。