C++模板編程:如何使非通用的模板函數實現聲明和定義分離


我們在編寫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中填寫的內容。

 

 


本文由 哈薩雅琪 原創,轉載請注明出處。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM