book
归档: C++ 
flag
mode_edit

dbg macro 真是奇妙

这个宏实现了一些很奇妙的反射操作,比如

  • 获取一个变量/常量的类型
  • 对于 STL 类型,输出容器中所有的值

如果让你来实现这个东西,你会怎么操作呢?

Type -> String

RTTI

一种很直接的思路是使用 RTTI:

template<typename T>
std::string type_of(T s) {
  return std::string(typeid(s).name());
}

看起来很不错!但是用起来好像不太好使:double 变成了 dchar变成了cchar [4] 变成了 A4_cvoid (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。现在问题有两个难点:

  1. 如何检测一个类型是否有 begin()end()
  2. 即使我们已经实现了对 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;
}
}
navigate_before navigate_next