从 type_info 判断子类(上)

我们知道 C++ 的 RTTI 能力有限,但是我一直不知道它这么有限。我们知道 dynamic_cast 能够让我们从一个父类指针测试是否能转换到子类指针,而子类指针是可以隐式的转换为父类指针的。然而在有 type erasure 的情况下问题就会发生变化。

为了获得类型的 type erasure 表达,通常有两个策略:

  • 通过 typeid(T) 获得对应的 std::type_info 引用或者构造 std::type_index
  • 通过一个整数

第二个方法虽然不是那么直接,我们参看下面的代码

template <typename T>
std::size_t fake_type_id() {
  static char dumb;
  return reinterpret_cast<std::size_t>(&dumb);
}

这样一来我们通过不同类型对应的 static variable 的地址作为这个类型的标识符。

有了这个表达我们可以轻松的通过判断两个 type erasure 来判定 strong type 是不是一致,这是实现诸如 boost.any 的终极原理。

struct any_ptr {
  using type_erasure = std::size_t;
  type_erasure type;
  void* ptr;

  template <typename T>
  T* as() {
    if (fake_type_id<T>() == type) {
      return static_cast<T*>(ptr);
    }
  }
};

template <typename T>
any_ptr make_any_ptr(T& t) {
  return any_ptr(fake_type_id<T>(), &t);
}

其实这里把 type_erasure 换为 std::type_index 然后获得的地方使用 std::type_index(typeid(T)) 也行。类似的 boost::typeindex 也提供了类似的功能,只是它封装的更加好一些。

一直有个疑问,就是当我们将 type erased 表示还原的时候为什么我们不能 cast 成为父类,比如下面的程序的输出就令人觉得很匪夷所思。

#include <boost/any.hpp>
#include <boost/type_index.hpp>
#include <iostream>

struct foo {
  virtual ~foo() {}

  int a;
};

struct bar {
  virtual ~bar() {}

  float b;
};

struct foobar : public foo, public bar {
  std::string c;
};

struct hello : public foobar {
  double d;
};

template <typename T> void try_cast(const boost::any &val) {
  try {
    T tmp = boost::any_cast<T>(val);
    std::cout << "casted into " << boost::typeindex::type_id<T>().pretty_name();
  } catch (const boost::bad_any_cast &) {
    std::cout << "not casted into "
              << boost::typeindex::type_id<T>().pretty_name();
  }
  std::cout << "\n";
}

int main() {
  boost::any a = hello();
  try_cast<hello>(a);
  try_cast<foobar>(a);
  try_cast<foo>(a);
  try_cast<bar>(a);

  return 0;
}

输出结果是

casted into hello
not casted into foobar
not casted into foo
not casted into bar

原因其实很简单,即便是标准的 type_info 也没有提供我们查询父类的功能。我一直以来以为 type_info::before 是指继承关系,最近仔细查看了这个函数才发现并非如此。下面的代码也可以证实这个想法是不对的

int main() {
  hello world;
  auto ptr = make_any_ptr(world);
  auto* hello_ptr = ptr.as<hello>();
  auto* foobar_ptr = ptr.as<foobar>();
  auto* foo_ptr = ptr.as<foo>();
  auto* bar_ptr = ptr.as<bar>();

  std::cout << "\nhello ptr: " << &world 
            << "\nfoobar ptr: " << static_cast<foobar*>(&world)
            << "\nfoo ptr: " << static_cast<foo*>(&world)
            << "\nbar ptr: " << static_cast<bar*>(&world);

  std::cout << "\nhello ptr: " << hello_ptr
            << "\nfoobar ptr: " << foobar_ptr
            << "\nfoo ptr: " << foo_ptr
            << "\nbar ptr: " << bar_ptr;

  std::cout << std::endl;

  return 0;
}

如果实现正确,两部分输出的应该是完全一样,但是实际上如果简单的 static_cast 就会出现问题(多继承也是其中一个重要的原因)。

那么是不是就没有办法了呢?其实所谓的 Itanium ABI 是提供了如此机制来搞定这个问题的。所幸 GCC/Clang 似乎都是支持这个功能,具体请看 cxxabi.h。下面是一个简化的实现:

bool is_base_of(const std::type_info& a, const std::type_info& b) {
  if (a == b) return true;
  const auto* bb = dynamic_cast<const __cxxabiv1::__class_type_info*>(&b);
  if (!bb) {
    return false;  // not even a class
  }
  const auto* si_bb = dynamic_cast<const __cxxabiv1::__si_class_type_info*>(bb);
  if (!si_bb) {
    return false;  // not supported
  }
  const auto* b_base = si_bb->__base_type;
  return &a == b_base || is_base_of(a, *b_base);
}

有了它,我们可以将我们的 any_ptr 弄成可以接受任意父类的样子。

从 type_info 判断子类(上)

留下评论