地球人都知道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您的返回结果真是。。) 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限定符和引用都被丢掉了=.= 一、如何检查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; }
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; } };
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)。 output out(str); out("Hello", "World", 123, "!");
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并不能保证永远返回一个有效的字符串。
所以说比较好的做法应该是在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[],所以会先匹配到指针的特化,因此*就被写到了最后面。 上面的第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指定一个其它的输出内容。 然后是[]的输出逻辑。考虑到对于[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]总是写在最后面的。 最后,是函数参数的输出逻辑。函数参数列表需要使用变参模板适配,用编译期递归的元编程手法输出参数,最后在两头加上括号。 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将只输出一对括号。 parameter<true, P...> parameter_;
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; 对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_, "," }); } };
3.2 数组(Arrays)的处理好了,有了上面的这些准备工作,写一个check的T[]特化是很简单的: 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 (*) []
check_type<const int[]>();
error: ambiguous class template instantiation for 'struct check<const int [], false>' check<T> { str }; ^ 检查了出错信息后,我们会惊讶的发现对于const int[]类型,竟然可以同时匹配T const和T[]。
可能描述有点晦涩,不过没关系,在8.3.4 Arrays的第1款最下面还有一行批注如下:
意思就是对于const int[]来说,const不仅属于数组里面的int元素所有,同时还会作用到数组本身上。 #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)
#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__继续向下传递时,变参为空逗号也不会消失。 然后,实现各种特化模板的时候到了: #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++中也是合法的。 当然,如果觉得这样不好看,想搞定这个也很简单,稍微改一下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_) {} };
std::cout << check_type<char(* (* const)(const int(&)[10]) )[10]>() << std::endl; // 输出:char (* (* const) (int const (&) [10])) [10] // 这是一个常函数指针,参数是一个常int数组的引用,返回值是一个char数组的指针 我们可以看到,函数指针已经被正确的处理掉了。这是因为一个函数指针会适配到指针上,之后去掉指针的类型将是一个正常的函数类型。 3.4 类成员指针(Pointers to members)的处理类成员指针的处理非常简单: 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...)的特化。 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...)显示完毕之后。
// 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_之后才被析构。 最后,来一起看看输出效果吧: 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++的类型系统是一个很有意思的事情。当钻进去之后就会发现,一些原先比较晦涩的基本概念,在研究的过程中都清晰了不少。 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:///cxx-get-the-name-of-the-given-type/) |
|
来自: astrotycoon > 《深度理解C 》