这个小巧的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类:
templatestruct 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 \ struct check : check \ { \ using base_t = check ; \ using base_t::out_; \ check(const output& out) : base_t(out) { out_(#OPT); } \ };
为了让外部的使用依旧简洁,实现一个外敷函数模板是很自然的事情:
templateinline std::string check_type(void) { std::string str; check { str }; return std::move(str); } int main(void) { std::cout << check_type () << 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()的结果。
一种健壮的写法可以像这样:
templatestruct 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 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[],所以会先匹配到指针的特化,因此*就被写到了最后面。
对于数组、函数等类型来说,若它们处在一个复合类型(compound types)中“子类型”的位置上,它们就需要用括号把它们的“父类型”给括起来。
因此我们还需要预先完成下面这些工作:
上面的第1点,可以利用模板偏特化这种静态的判断来解决。比如说,给check添加一个默认的bool模板参数:
templatestruct check { // ... }; #define CHECK_TYPE__(OPT) \ template \ struct check : check \ { \ using base_t = check ; \ using base_t::out_; \ check(const output& out) : base_t(out) { out_(#OPT); } \ };
这个小小的修改就可以让check在继承的时候把父-子信息传递下去。
接下来先考虑圆括号的输出逻辑。我们可以构建一个bracket类,在编译期帮我们自动处理圆括号:
// () templatestruct bracket { output& out_; bracket(output& out, const char* = nullptr) : out_(out) { out_("(").compact(); } ~bracket(void) { out_.compact()(")"); } }; template <> struct bracket { bracket(output& out, const char* str = nullptr) { out(str); } };
在bracket里,不仅实现了圆括号的输出,其实还实现了一个编译期if的小功能。当不输出圆括号时,我们可以给bracket指