dbg macro 真是奇妙
这个宏实现了一些很奇妙的反射操作,比如
- 获取一个变量/常量的类型
- 对于 STL 类型,输出容器中所有的值
如果让你来实现这个东西,你会怎么操作呢?
Type -> String
RTTI
一种很直接的思路是使用 RTTI:
template<typename T>
std::string type_of(T s) {
return std::string(typeid(s).name());
}
看起来很不错!但是用起来好像不太好使:double
变成了 d
,char
变成了c
,char [4]
变成了 A4_c
,void (int)
变成了 FivE
之类的奇怪东西,cppref 贴心地提醒我们,毒瘤 gcc 和 clang 返回的都是经过重整后的类型,还要过一下 c++filt -t
才行,或者用 abi::__cxa_demangle
才能得到人类可读的类型:
template<typename T>
string type_of(T s) {
size_t sz = 1000;
char res[1000];
int status = 0;
abi::__cxa_demangle(typeid(s).name(), res, &sz, &status);
return std::string(res);
}
这下应该能用了!来试一下:
int a = 0;
const int &b = a;
std::cout << type_of(a) << ' ' << type_of(b) << std::endl;
// 输出:
// int int
由于 typeid
会直接忽略 c(onst)v(olatile) 限定符,所以这种情况是无法区分的。RTTI死路一条
魔法时间
dbg.h
使用了一些奇怪的技巧。
首先考虑 RTTI 的问题:无法区分 cv 限定符。那么应该怎么办呢?来看一下 <type_traits>
里面的一些类模板:
类模板 | 作用 |
---|---|
is_pointer(C++11) | 检查类型是否为指针类型 (类模板) |
is_lvalue_reference(C++11) | 检查类型是否为左值引用 (类模板) |
is_rvalue_reference(C++11) | 检查类型是否为右值引用 (类模板) |
is_const(C++11) | 检查类型是否为 const 限定 (类模板) |
is_volatile(C++11) | 检查类型是否为 volatile 限定 (类模板) |
好像很好使的样子!
类模板 | 作用 |
---|---|
remove_cv remove_const remove_volatile (C++11) | 从给定类型移除 const 或/与 volatile 限定符 (类模板) |
remove_reference (C++11) | 从给定类型移除引用 |
remove_pointer (C++11) | 移除给定类型的一层指针 |
似乎已经有点思路了,只需要判定一个类型是不是 指针/引用/cv 然后再去除这些限定符,递归解析剩下的类型就可以了。
// modified from dbg.h
template <typename T> std::string type_name() {
if (std::is_volatile<T>::value) {
if (std::is_pointer<T>::value) {
return type_name<typename std::remove_volatile<T>::type>() + " volatile";
} else {
return "volatile " + type_name<typename std::remove_volatile<T>::type>();
}
}
if (std::is_const<T>::value) {
if (std::is_pointer<T>::value) {
return type_name<typename std::remove_const<T>::type>() + " const";
} else {
return "const " + type_name<typename std::remove_const<T>::type>();
}
}
if (std::is_pointer<T>::value) {
return type_name<typename std::remove_pointer<T>::type>() + "*";
}
if (std::is_lvalue_reference<T>::value) {
return type_name<typename std::remove_reference<T>::type>() + "&";
}
if (std::is_rvalue_reference<T>::value) {
return type_name<typename std::remove_reference<T>::type>() + "&&";
}
return get_type_name<T>(); // TODO: impl get_type_name
}
由于最后的类型已经没有 cv 限定符和引用,因此可以直接用我们之前实现的 type_of
来实现,但是 dbg.h
使用了一种更奇妙的实现,具体可以自己去 review 代码。
偏特化一类类型
没有 typeclass/trait/interface/concept(?) 的辣鸡语言写起来真是费劲
下面我们要实现一个 to_string
,对于 STL 类型(准确来说,是实现了 begin()
和 end()
的类),我们输出它的所有元素;对于剩下的类型,我们直接调用 std::to_string
。现在问题有两个难点:
- 如何检测一个类型是否有
begin()
和end()
? - 即使我们已经实现了对
begin()
和end()
的检测,应该怎么大战编译器,才能让编译器为我们分别对这两类类型生成两种函数?
S(ubstitution)F(ailure)I(s)N(ot)A(n)E(rror)
强烈推荐 An introduction to C++’s SFINAE concept: compile-time introspection of a class member,我很难写的比他更好。
根据这篇文章的提示,我们写出下面的代码:
template <typename, typename = void> struct is_stl : std::false_type {};
template <typename T>
struct is_stl<T, std::void_t<decltype(std::declval<T>().begin(),
std::declval<T>().end())>>
: std::true_type {};
注意,decltype
里是逗号表达式,而非多个参数。如果 T 不是 STL,那么第二个就会替换失败,fallback 到第一行的 false_type
上。
下面解决如何让编译器给我们生成想要的函数:
template <typename T, typename U>
using stl = typename std::enable_if<is_stl<T>::value, U>::type;
template <typename T, typename U>
using nonstl = typename std::enable_if<!is_stl<T>::value, U>::type;
然后就可以写出这样的东西:
namespace mgt {
template <typename T> nonstl<T, std::string> to_string(const T& v) {
return std::to_string(v);
}
template <typename T> stl<T, std::string> to_string(const T& vec) {
string res = "[";
for (auto& i : vec) {
res += mgt::to_string(i);
res += ", ";
}
if (!res.empty())
res.pop_back(), res.pop_back();
res += ']';
return res;
}
}