怎样在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/)


    
  • posted @ 2015-12-20 09:34  zfyouxi  阅读(13305)  评论(0编辑  收藏  举报