怎樣在C++中獲得完整的類型名稱



地球人都知道C++里有一個typeid操作符能夠用來獲取一個類型/表達式的名稱:

std::cout << typeid(int).name() << std::endl;

可是這個name()的返回值是取決於編譯器的。在vc和gcc中打印出來的結果例如以下:

int // vc
i   // gcc

一個略微長一點的類型名稱。比方:

class Foo {};
std::cout << typeid(Foo*[10]).name() << std::endl;


打出來是這個效果:

class Foo * [10] // vc
A10_P3Foo        // gcc

(話說gcc您的返回結果真是。。


當然了,想在gcc里得到和微軟差點兒相同顯示效果的方法也是有的。那就是使用__cxa_demangle

char* name = abi::__cxa_demangle(typeid(Foo*[10]).name(), nullptr, nullptr, nullptr);
std::cout << name << std::endl;
free(name);

顯示效果:

Foo* [10]

先不說不同編譯器下的適配問題,來看看以下這個會打印出啥:

// vc
std::cout << typeid(const int&).name() << std::endl;
 
// gcc
char* name = abi::__cxa_demangle(typeid(const int&).name(), nullptr, nullptr, nullptr);
std::cout << name << std::endl;
free(name);

顯示效果:

int // vc
int // gcc

可愛的cv限定符和引用都被丟掉了=.=
假設直接在typeid的結果上加上被丟棄的信息,對於一些類型而言(如函數指針引用)得到的將不是一個正確的類型名稱。
想要獲得一個類型的完整名稱。而且獲得的名稱必需要是一個正確的類型名稱,應該如何做呢?

一、怎樣檢查C++中的類型

我們須要一個泛型類,用特化/偏特化機制靜態檢查出C++中的各種類型,而且不能忽略掉類型限定符(type-specifiers)和各種聲明符(declarators)。


先來考慮一個最簡單的類模板:

template <typename T>
struct check
{
    // ...
};


假如在它的基礎上特化,須要寫多少個版本號呢?我們能夠略微實現下試試:

template <typename T> struct check<T &>;
template <typename T> struct check<T const &>;
template <typename T> struct check<T volatile &>;
template <typename T> struct check<T const volatile &>;
 
template <typename T> struct check<T &&>;
template <typename T> struct check<T const &&>;
template <typename T> struct check<T volatile &&>;
template <typename T> struct check<T const volatile &&>;
 
template <typename T> struct check<T *>;
template <typename T> struct check<T const *>;
template <typename T> struct check<T volatile *>;
template <typename T> struct check<T const volatile *>;
template <typename T> struct check<T * const>;
template <typename T> struct check<T * volatile>;
template <typename T> struct check<T * const volatile>;
 
template <typename T> struct check<T []>;
template <typename T> struct check<T const []>;
template <typename T> struct check<T volatile []>;
template <typename T> struct check<T const volatile []>;
template <typename T, size_t N> struct check<T [N]>;
template <typename T, size_t N> struct check<T const [N]>;
template <typename T, size_t N> struct check<T volatile [N]>;
template <typename T, size_t N> struct check<T const volatile [N]>;
 
// ......

這還遠遠沒有完。有同學可能會說了,我們不是有偉大的宏嘛。這些東西都像是一個模子刻出來的,弄一個宏批量生成下不就完了。

實際上當我們真的信心滿滿的動手去寫這些宏的時候。才發現適配上的細微區別會讓宏寫得很痛苦(比方&和*的區別,[]和[N]的區別,還有函數類型、函數指針、函數指針引用、函數指針數組、類成員指針、……)。當我們一一羅列出須要特化的細節時,不由得感嘆C++類型系統的復雜和糾結。

可是上面的理由並非這個思路的致命傷。
不可行的地方在於:我們能夠寫一個多維指針,或多維數組,類型是能夠嵌套的。總不可能為每個維度都特化一個模板吧。

只是正因為類型事實上是嵌套的。我們能夠用模板元編程的基本思路來搞定這個問題:

template <typename T> struct check<T const> : check<T>;
template <typename T> struct check<T volatile> : check<T>;
template <typename T> struct check<T const volatile> : check<T>;
 
template <typename T> struct check<T & > : check<T>;
template <typename T> struct check<T &&> : check<T>;
template <typename T> struct check<T * > : check<T>;
 
// ......

一個簡單的繼承,就讓特化變得simple非常多。由於當我們萃取出一個類型。比方T *,之后的T事實上是攜帶上了除*之外全部其它類型信息的一個類型。

那么把這個T再反復投入check中,就會繼續萃取它的下一個類型特征。

能夠先用指針、引用的萃取來看看效果:

#include <iostream>
 
template <typename T>
struct check
{
    check(void)  { std::cout << typeid(T).name(); }
    ~check(void) { std::cout << std::endl; }
};
 
#define CHECK_TYPE__(OPT) \
    template <typename T> \
    struct check<T OPT> : check<T> \
    { \
        check(void) { std::cout << " "#OPT; } \
    };
 
CHECK_TYPE__(const)
CHECK_TYPE__(volatile)
CHECK_TYPE__(const volatile)
CHECK_TYPE__(&)
CHECK_TYPE__(&&)
CHECK_TYPE__(*)
 
int main(void)
{
    check<const volatile void * const*&>();
    system("pause");
    return 0;
}


輸出結果(vc):

void const volatile * const * &

非常美麗,是不是?當然,在gcc里這樣輸出,void會變成v,所以gcc以下要這樣寫check模板:

template <typename T>
struct check
{
    check(void)
    {
        char* real_name = abi::__cxa_demangle(typeid(T).name(), nullptr, nullptr, nullptr);
        std::cout << real_name;
        free(real_name);
    }
    ~check(void) { std::cout << std::endl; }
};

二、保存和輸出字符串

我們能夠簡單的這樣改動check讓它同一時候支持vc和gcc:

template <typename T>
struct check
{
    check(void)
    {
#   if defined(__GNUC__)
        char* real_name = abi::__cxa_demangle(typeid(T).name(), nullptr, nullptr, nullptr);
        std::cout << real_name;
        free(real_name);
#   else
        std::cout << typeid(T).name();
#   endif
    }
    ~check(void) { std::cout << std::endl; }
};


可是到眼下為止,check的輸出結果都是無法保存的。比較好的方式是可以像typeid(T).name()一樣返回一個字符串。

這就要求check可以把結果保存在一個std::string對象里。
當然了,我們能夠直接給check一個“std::string& out”類型的構造函數,可是這樣會把輸出的狀態管理、字符的打印邏輯等等都揉在一起。因此。比較好的設計方法是實現一個output類。負責輸出和維護狀態。我們到后面就會慢慢感覺到這樣做的優點在哪里。
output類的實現能夠是這樣:

class output
{
    bool is_compact_ = true;
 
    template <typename T>
    bool check_empty(const T&) { return false; }
    bool check_empty(const char* val)
    {
        return (!val) || (val[0] == 0);
    }
 
    template <typename T>
    void out(const T& val)
    {
        if (check_empty(val)) return;
        if (!is_compact_) sr_ += " ";
        using ss_t = std::ostringstream;
        sr_ += static_cast<ss_t&>(ss_t() << val).str();
        is_compact_ = false;
    }
 
    std::string& sr_;
 
public:
    output(std::string& sr) : sr_(sr) {}
 
    output& operator()(void) { return (*this); }
 
    template <typename T1, typename... T>
    output& operator()(const T1& val, const T&... args)
    {
        out(val);
        return operator()(args...);
    }
 
    output& compact(void)
    {
        is_compact_ = true;
        return (*this);
    }
};

這個小巧的output類負責自己主動管理輸出狀態(是否添加空格)和輸出的類型轉換(使用std::ostringstream)。
上面的實現里有兩個比較有意思的地方。
一是operator()的做法,採用了變參模板。這樣的做法讓我們能夠這樣用output:

output out(str);
out("Hello", "World", 123, "!");


這樣的寫法比cout的流操作符舒服多了。


二是operator()和compact的返回值。當然,這里能夠直接使用void,可是這會造成一些限制。
比方說,我們想在使用operator()之后立即compact呢?若讓函數返回自身對象的引用,就能夠讓output用起來很順手:

output out(str);
out.compact()("Hello", "World", 123, "!").compact()("?

");


check的定義和CHECK_TYPE__宏僅僅須要略作改動就能夠使用output類:

template <typename T>
struct check
{
    output out_;
    check(const output& out) : out_(out)
    {
#   if defined(__GNUC__)
        char* real_name = abi::__cxa_demangle(typeid(T).name(), nullptr, nullptr, nullptr);
        out_(real_name);
        free(real_name);
#   else
        out_(typeid(T).name());
#   endif
    }
};
 
#define CHECK_TYPE__(OPT) \
    template <typename T> \
    struct check<T OPT> : check<T> \
    { \
        using base_t = check<T>; \
        using base_t::out_; \
        check(const output& out) : base_t(out) { out_(#OPT); } \
    };


為了讓外部的使用依然簡潔,實現一個外敷函數模板是非常自然的事情:

template <typename T>
inline std::string check_type(void)
{
    std::string str;
    check<T> { str };
    return std::move(str);
}
 
int main(void)
{
    std::cout << check_type<const volatile void * const*&>() << std::endl;
    system("pause");
    return 0;
}

假設我們想實現表達式的類型輸出,使用decltype包裹一下即可了。

不知道看到這里的朋友有沒有注意到,check在gcc下的輸出可能會出現故障。原因是abi::__cxa_demangle並不能保證永遠返回一個有效的字符串。
我們來看看這個函數的返回值說明

“Returns: A pointer to the start of the NUL-terminated demangled name, or NULL if the demangling fails. The caller is responsible for deallocating this memory using free.”

所以說比較好的做法應該是在abi::__cxa_demangle返回空的時候,直接使用typeid(T).name()的結果。


一種健壯的寫法能夠像這樣:

template <typename T>
struct check
{
    output out_;
    check(const output& out) : out_(out)
    {
#   if defined(__GNUC__)
        const char* typeid_name = typeid(T).name();
        auto deleter = [](char* p)
        {
            if (p) free(p);
        };
        std::unique_ptr<char, decltype(deleter)> real_name
        {
            abi::__cxa_demangle(typeid_name, nullptr, nullptr, nullptr), deleter
        };
        out_(real_name ?

real_name.get() : typeid_name); # else out_(typeid(T).name()); # endif } };


上面我們通過使用std::unique_ptr配合lambda的自己定義deleter。實現了一個簡單的Scope Guard機制。來保證當abi::__cxa_demangle返回的非NULL指針一定會被free掉。

三、輸出有效的類型定義

3.1 一些准備工作

上面的特化攻克了cv限定符、引用和指針,甚至對於未特化的數組、類成員指針等都有還不錯的顯示效果,只是卻無法保證輸出的類型名稱一定是一個有效的類型定義。比方說:

check_type<int(*)[]>(); // int [] *

原因是由於這個類型是一個指針,指向一個int[]。所以會先匹配到指針的特化,因此*就被寫到了最后面。
對於數組、函數等類型來說,若它們處在一個復合類型(compound types)中“子類型”的位置上。它們就須要用括號把它們的“父類型”給括起來。
因此我們還須要預先完畢以下這些工作:

  • 1. 怎樣推斷數組、函數等類型的特化處於check繼承鏈中“被繼承”(也就是某個類的基類)的位置上
  • 2. 圓括號()、方括號[],以及函數參數列表的輸出邏輯

    上面的第1點,能夠利用模板偏特化這樣的靜態的推斷來解決。比方說。給check加入一個默認的bool模板參數:

    template <typename T, bool IsBase = false>
    struct check
    {
        // ...
    };
     
    #define CHECK_TYPE__(OPT) \
        template <typename T, bool IsBase> \
        struct check<T OPT, IsBase> : check<T, true> \
        { \
            using base_t = check<T, true>; \
            using base_t::out_; \
            check(const output& out) : base_t(out) { out_(#OPT); } \
        };

    這個小小的改動就能夠讓check在繼承的時候把父-子信息傳遞下去。

    接下來先考慮圓括號的輸出邏輯。我們能夠構建一個bracket類。在編譯期幫我們自己主動處理圓括號:

    // ()
     
    template <bool>
    struct bracket
    {
        output& out_;
     
        bracket(output& out, const char* = nullptr) : out_(out)
        { out_("(").compact(); }
     
        ~bracket(void)
        { out_.compact()(")"); }
    };
     
    template <>
    struct bracket<false>
    {
        bracket(output& out, const char* str = nullptr)
        { out(str); }
    };

    在bracket里,不僅實現了圓括號的輸出,事實上還實現了一個編譯期if的小功能。當不輸出圓括號時,我們能夠給bracket指定一個其他的輸出內容。


    當然,不實現bracket,直接在check的類型特化里處理括號邏輯也能夠。可是這種話邏輯就被某個check特化綁死了。我們能夠看到bracket的邏輯被剝離出來以后。后面全部須要輸出圓括號的部分都能夠直接復用這個功能。

    然后是[]的輸出邏輯。考慮到對於[N]類型的數組。還須要把N的詳細數值輸出來。因此輸出邏輯能夠這樣寫:

    // [N]
     
    template <size_t N = 0>
    struct bound
    {
        output& out_;
     
        bound(output& out) : out_(out) {}
        ~bound(void)
        {
            if (N == 0) out_("[]");
            else        out_("[").compact()
                            ( N ).compact()
                            ("]");
        }
    };

    輸出邏輯須要寫在bound類的析構。而不是構造里。原因是對於一個數組類型,[N]總是寫在最后面的。
    這里在輸出的時候直接使用了執行時的if-else,而沒有再用特化來處理。是由於當N是一個編譯期數值時,對於現代的編譯器來說“if (N == 0) ; else ;”語句會被優化掉。僅僅生成確定邏輯的匯編碼。

    最后。是函數參數的輸出邏輯。

    函數參數列表須要使用變參模板適配,用編譯期遞歸的元編程手法輸出參數,最后在兩頭加上括號。


    我們能夠先寫出遞歸的結束條件:

    template <bool, typename... P>
    struct parameter;
     
    template <bool IsStart>
    struct parameter<IsStart>
    {
        output& out_;
     
        parameter(output& out) : out_(out) {}
        ~parameter(void)
        { bracket<IsStart> { out_ }; }
    };

    輸出邏輯寫在析構里的理由,和bound一致。結束條件是顯然的:當參數包為空時。parameter將僅僅輸出一對括號。
    注意到模板的bool類型參數,讓我們在使用的時候須要這樣寫:

    parameter<true, P...> parameter_;


    這是由於bool模板參數混在變參里,指定默認值也是沒辦法省略true的。
    略微有點復雜的是參數列表的輸出。一個簡單的寫法是這樣:

    template <bool IsStart, typename P1, typename... P>
    struct parameter<IsStart, P1, P...>
    {
        output& out_;
     
        parameter(output& out) : out_(out) {}
        ~parameter(void)
        {
            bracket<IsStart> bk { out_, "," }; (void)bk;
            check<P1> { out_ };
            parameter<false, P...> { out_.compact() };
        }
    };

    parameter在析構的時候,析構函數的scope就是bracket的影響范圍。后面的其他顯示內容,都應該被包含在bracket之內,因此bracket須要顯式定義暫時變量bk;
    check的調用理由非常easy。由於我們須要顯示出每一個參數的詳細類型;
    最以下是parameter的遞歸調用。在把out_丟進去之前,我們須要思考下詳細的顯示效果。是希望打印出(P1, P2, P3)呢,還是(P1 , P2 , P3)?
    在這里我們選擇了逗號之前沒有空格的第一個版本號。因此給parameter傳遞的是out_.compact()。

    對parameter的代碼來說。看起來不明顯的就是bracket的作用域了,check和parameter的調用事實上是被bracket包圍住的。為了強調bracket的作用范圍。同一時候規避掉莫名其妙的“(void)bk;”手法,我們能夠使用lambda表達式來凸顯邏輯:

    template <bool IsStart, typename P1, typename... P>
    struct parameter<IsStart, P1, P...>
    {
        output& out_;
     
        parameter(output& out) : out_(out) {}
        ~parameter(void)
        {
            [this](bracket<IsStart>&&)
            {
                check<P1> { out_ };
                parameter<false, P...> { out_.compact() };
            } (bracket<IsStart> { out_, "," });
        }
    };


    這樣bracket的作用域一目了然,而且和check、parameter的定義方式保持一致。同一時候也更easy看出來out_.compact()的意圖。

    3.2 數組(Arrays)的處理

    好了,有了上面的這些准備工作。寫一個check的T[]特化是非常easy的:

    template <typename T, bool IsBase>
    struct check<T[], IsBase> : check<T, true>
    {
        using base_t = check<T, true>;
        using base_t::out_;
     
        bound<>         bound_;
        bracket<IsBase> bracket_;
     
        check(const output& out) : base_t(out)
            , bound_  (out_)
            , bracket_(out_)
        {}
    };


    這時對於不指定數組長度的[]類型,輸出結果例如以下:

    check_type<int(*)[]>(); // int (*) []


    當我們開始興致勃勃的接着追加[N]的模板特化之前。須要先檢查下cv的檢查機制是否運作良好:

    check_type<const int[]>();


    嘗試編譯時,gcc會給我們吐出一堆類似這種compile error:

    error: ambiguous class template instantiation for 'struct check<const int [], false>'
         check<T> { str };
         ^

    檢查了出錯信息后。我們會吃驚的發現對於const int[]類型,居然能夠同一時候匹配T const和T[]。
    這是由於依照C++標准ISO/IEC-14882:2011,3.9.3 CV-qualifiers。第5款:

    “Cv-qualifiers applied to an array type attach to the underlying element type, so the notation “cv T,” where T is an array type, refers to an array whose elements are so-qualified. Such array types can be said to be more (or less) cv-qualified than other types based on the cv-qualification of the underlying element types.”

    可能描寫敘述有點晦澀,只是沒關系,在8.3.4 Arrays的第1款最以下另一行批注例如以下:

    “[ Note: An “array of N cv-qualifier-seq T” has cv-qualified type; see 3.9.3. —end note ]”

    意思就是對於const int[]來說。const不僅屬於數組里面的int元素全部,同一時候還會作用到數組本身上。
    所以說,我們不得不多做點工作。把cv限定符也特化進來:

    #define CHECK_TYPE_ARRAY__(CV_OPT) \
        template <typename T, bool IsBase> \
        struct check<T CV_OPT [], IsBase> : check<T CV_OPT, true> \
        { \
            using base_t = check<T CV_OPT, true>; \
            using base_t::out_; \
        \
            bound<>         bound_; \
            bracket<IsBase> bracket_; \
        \
            check(const output& out) : base_t(out) \
                , bound_  (out_) \
                , bracket_(out_) \
            {} \
        };
     
    #define CHECK_TYPE_PLACEHOLDER__
    CHECK_TYPE_ARRAY__(CHECK_TYPE_PLACEHOLDER__)
    CHECK_TYPE_ARRAY__(const)
    CHECK_TYPE_ARRAY__(volatile)
    CHECK_TYPE_ARRAY__(const volatile)


    這樣對於加了cv屬性的數組而言,編譯和顯示才是正常的。
    接下來。考慮[N],我們須要略微改動一下上面的CHECK_TYPE_ARRAY__宏。讓它能夠同一時候處理[]和[N]:

    #define CHECK_TYPE_ARRAY__(CV_OPT, BOUND_OPT, ...) \
        template <typename T, bool IsBase __VA_ARGS__> \
        struct check<T CV_OPT [BOUND_OPT], IsBase> : check<T CV_OPT, true> \
        { \
            using base_t = check<T CV_OPT, true>; \
            using base_t::out_; \
        \
            bound<BOUND_OPT> bound_; \
            bracket<IsBase>  bracket_; \
        \
            check(const output& out) : base_t(out) \
                , bound_  (out_) \
                , bracket_(out_) \
            {} \
        };
     
    #define CHECK_TYPE_ARRAY_CV__(BOUND_OPT, ...) \
        CHECK_TYPE_ARRAY__(, BOUND_OPT, ,##__VA_ARGS__) \
        CHECK_TYPE_ARRAY__(const, BOUND_OPT, ,##__VA_ARGS__) \
        CHECK_TYPE_ARRAY__(volatile, BOUND_OPT, ,##__VA_ARGS__) \
        CHECK_TYPE_ARRAY__(const volatile, BOUND_OPT, ,##__VA_ARGS__)

    這段代碼里略微用了點“preprocessor”式的技巧。gcc的__VA_ARGS__處理事實上不那么人性化。

    盡管我們能夠通過“,##__VA_ARGS__”。在變參為空時消除掉前面的逗號,但這個機制卻僅僅對第一層宏有效。當我們把__VA_ARGS__繼續向下傳遞時,變參為空逗號也不會消失。
    因此,我們僅僅實用上面這樣的略顯抽搐的寫法來干掉第二層宏里的逗號。這個處理技巧也相同適用於vc。

    然后,實現各種特化模板的時候到了:

    #define CHECK_TYPE_PLACEHOLDER__
    CHECK_TYPE_ARRAY_CV__(CHECK_TYPE_PLACEHOLDER__)
    #if defined(__GNUC__)
    CHECK_TYPE_ARRAY_CV__(0)
    #endif
    CHECK_TYPE_ARRAY_CV__(N, size_t N)

    這里有個有意思的地方是:gcc里能夠定義0長數組[0]。也叫“柔性數組”。這玩意在gcc里不會適配到T[N]或T[]上。所以要單獨考慮。

    如今,我們適配上了全部的引用、數組,以及普通指針:

    check_type<const volatile void *(&)[10]>(); // void const volatile * (&) [10]
    check_type<int [1][2][3]>();                // int (([1]) [2]) [3]
    這里看起來有點不一樣的是多維數組的輸出結果,每一個維度都被括號限定了結合范圍。

    這樣的用括號明白標明數組每一個維度的結合優先級的寫法。盡管看起來不那么干脆,只是在C++中也是合法的。
    當然。假設認為這樣不好看,想搞定這個也非常easy。略微改一下CHECK_TYPE_ARRAY__就能夠了:

    #define CHECK_TYPE_ARRAY__(CV_OPT, BOUND_OPT, ...) \
        template <typename T, bool IsBase __VA_ARGS__> \
        struct check<T CV_OPT [BOUND_OPT], IsBase> : check<T CV_OPT, !std::is_array<T>::value> \
        { \
            using base_t = check<T CV_OPT, !std::is_array<T>::value>; \
            using base_t::out_; \
        \
            bound<BOUND_OPT> bound_; \
            bracket<IsBase>  bracket_; \
        \
            check(const output& out) : base_t(out) \
                , bound_  (out_) \
                , bracket_(out_) \
            {} \
        };

    這里使用了std::is_array來推斷下一層類型是否仍舊是數組。假設是的話,則不輸出括號。

    3.3 函數(Functions)的處理

    有了前面准備好的parameter,實現一個函數的特化處理很輕松:

    template <typename T, bool IsBase, typename... P>
    struct check<T(P...), IsBase> : check<T, true>
    {
        using base_t = check<T, true>;
        using base_t::out_;
     
        parameter<true, P...> parameter_;
        bracket<IsBase>       bracket_;
     
        check(const output& out) : base_t(out)
            , parameter_(out_)
            , bracket_  (out_)
        {}
    };


    這里有一個小注意點:函數和數組一樣,處於被繼承的位置時須要加括號;parameter的構造時機應該在bracket的前面。這樣能夠保證它在bracket之后被析構。否則參數列表將被加入到錯誤位置上。


    我們能夠打印一個變態一點的類型來驗證下正確性:


    std::cout << check_type<char(* (* const)(const int(&)[10]) )[10]>() << std::endl;
    // 輸出:char (* (* const) (int const (&) [10])) [10]
    // 這是一個常函數指針,參數是一個常int數組的引用。返回值是一個char數組的指針

    我們能夠看到。函數指針已經被正確的處理掉了。

    這是由於一個函數指針會適配到指針上,之后去掉指針的類型將是一個正常的函數類型。
    這里我們沒有考慮stdcall、fastcall等調用約定的處理,如有須要的話,讀者可自行加入。

    3.4 類成員指針(Pointers to members)的處理

    類成員指針的處理很easy:

    template <typename T, bool IsBase, typename C>
    struct check<T C::*, IsBase> : check<T, true>
    {
        using base_t = check<T, true>;
        using base_t::out_;
     
        check(const output& out) : base_t(out)
        {
            check<C> { out_ };
            out_.compact()("::*");
        }
    };


    顯示效果:


    class Foo {};
    std::cout << check_type<int (Foo::* const)[3]>() << std::endl;
    // 輸出:int (Foo::* const) [3]
    // 這是一個常類成員指針。指向Foo里的一個int[3]成員

    3.5 類成員函數指針(Pointers to member functions)的處理

    事實上我們不用做什么特別的處理,通過T C::*已經能夠適配無cv限定符的普通類成員函數指針了。僅僅是在vc下,提取出來的T卻無法適配上T(P...)的特化。
    這是由於vc中通過T C::*提取出來的函數類型帶上了一個隱藏的thiscall調用約定。在vc里,我們無法聲明或定義一個thiscall的普通函數類型,於是T C::*的特化適配無法完美的達到我們想要的效果。


    所以。我們還是須要處理無cv限定的類成員函數指針。通過一個和上面T C::*的特化非常像的特化模板。就能夠處理掉一般的類成員函數指針:

    template <typename T, bool IsBase, typename C, typename... P>
    struct check<T(C::*)(P...), IsBase> : check<T(P...), true>
    {
        using base_t = check<T(P...), true>;
        using base_t::out_;
     
        check(const output& out) : base_t(out)
        {
            check<C> { out_ };
            out_.compact()("::*");
        }
    };

    以下考慮帶cv限定符的類成員函數指針。在開始書寫后面的代碼之前,我們須要先思考一下,cv限定符在類成員函數指針上的顯示位置是哪里?答案當然是在函數的參數表后面。

    所以我們必須把cv限定符的輸出時機放在T(P...)顯示完成之后。


    因此想要正確的輸出cv限定符。我們必須調整T(P...)特化的調用時機:


    // Do output at destruct
     
    struct at_destruct
    {
        output&     out_;
        const char* str_;
     
        at_destruct(output& out, const char* str = nullptr)
            : out_(out)
            , str_(str)
        {}
        ~at_destruct(void)
        { out_(str_); }
     
        void set_str(const char* str = nullptr)
        { str_ = str; }
    };
     
    #define CHECK_TYPE_MEM_FUNC__(...) \
        template <typename T, bool IsBase, typename C, typename... P> \
        struct check<T(C::*)(P...) __VA_ARGS__, IsBase> \
        { \
            at_destruct cv_; \
            check<T(P...), true> base_; \
            output& out_ = base_.out_; \
        \
            check(const output& out) \
                : cv_(base_.out_) \
                , base_(out) \
            { \
                cv_.set_str(#__VA_ARGS__); \
                check<C> { out_ }; \
                out_.compact()("::*"); \
            } \
        };
     
    CHECK_TYPE_MEM_FUNC__()
    CHECK_TYPE_MEM_FUNC__(const)
    CHECK_TYPE_MEM_FUNC__(volatile)
    CHECK_TYPE_MEM_FUNC__(const volatile)

    上面這段代碼先定義了一個at_destruct,用來在析構時運行“輸出cv限定符”的動作;同一時候把原本處在基類位置上的T(P...)特化放在了第二成員的位置上,這樣就保證了它將會在cv_之后才被析構。
    這里要注意的是,at_destruct的構造在base_和out_之前。所以假設直接給cv_傳遞out_時不行的。這個時候out_還沒有初始化呢。可是在這個時候,盡管base_相同尚未初始化,但base_.out_的引用卻是有效的,因此我們能夠給cv_傳遞一個base_.out_。
    另外。at_destruct盡管定義了帶str參數的構造函數,CHECK_TYPE_MEM_FUNC__宏中卻沒有使用它。

    原因是若在宏中使用#__VA_ARGS__作為參數,那么當變參為空時,#__VA_ARGS__前面的逗號在vc中不會被自己主動忽略掉(gcc會忽略)。

    最后。來一起看看輸出效果吧:

    class Foo {};
    std::cout << check_type<int (Foo::* const)(int, Foo&&, int) volatile>() << std::endl;
    // 輸出:int (Foo::* const) (int, Foo &&, int) volatile
    // 這是一個常類成員函數指針,指向Foo里的一個volatile成員函數

    尾聲

    折騰C++的類型系統是一個非常有意思的事情。當鑽進去之后就會發現,一些原先比較晦澀的基本概念。在研究的過程中都清晰了不少。
    check_type的有用價值在於。能夠利用它清晰的看見C++中一些隱藏的類型變化。比方完美轉發時的引用折疊:

    class Foo {};
     
    template <typename T>
    auto func(T&&) -> T;
     
    std::cout << check_type<decltype(func<Foo>)>() << std::endl;
    std::cout << check_type<decltype(func<Foo&>)>() << std::endl;
    std::cout << check_type<decltype(func<Foo&&>)>() << std::endl;

    在上面實現check_type的過程中。用到了不少泛型,甚至元編程的小技巧。充分運用了C++在預處理期、編譯期和執行期(RAII)的處理能力。盡管這些代碼僅是學習研究時的興趣之作,實際項目中往往typeid的返回結果就足夠了。但上面的不少技巧對一些現實中的項目開發也有一定的參考和學習價值。

    順便說一下:上面的代碼里使用了大量C++11的特征。若想在老C++中實現check_type,大部分的新特征也都能夠找到替代的手法。僅僅是適配函數類型時使用的變參模板。在C++98/03下實現起來實在抽搐。

    論代碼的表現力和舒適度,C++11強過C++98/03太多了。


    完整代碼及測試下載請點擊:check_type

    Wrote by mutouyun. (http://darkc.at/cxx-get-the-name-of-the-given-type/)


    

  • 免責聲明!

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



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