我们知道 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 弄成可以接受任意父类的样子。