参考原文地址:(原创)用 C++11 打造好用的 any
我对文章的格式和错别字进行了调整,并标注出了重要的部分。以下是正文。
正文
上一篇博文用 C++11 实现了 variant,有童鞋说何不把 any 也实现一把,我正有此意,它的兄弟 variant 已经实现了,any 也顺便打包实现了吧。其实 boost.any 已经挺好了,就是转换异常时,看不到详情,和 boost.variant 一样的问题。实现 any 比实现 variant 要简单,需要解决的关键技术是类型擦除,关于类型擦除我之前的博文有介绍,想了解的童鞋点这里。
实现 any 的关键技术
Any 能容纳所有类型的数据,因此当赋值给 Any 时,需要将值的类型擦除才行,即以一种通用的方式保存所有类型的数据。这里可以通过继承去擦除类型,基类是不含模板参数的,派生类中才有模板参数,这个模板参数类型正是赋值的类型,在赋值时,将创建的派生类对象赋值给基类指针,基类的派生类中携带了数据类型,基类只是原始数据的一个占位符,通过多态,它擦除了原始数据类型,因此,任何数据类型都可以赋值给它,从而实现了能存放所有类型数据的目标。当取数据时需要向下转换成派生类型来获取原始数据,当转换失败时打印详情,并抛出异常。由于 Any 赋值时需要创建一个派生类对象,所以还需要管理该对象的生命周期,这里用 unique_ptr 智能指针去管理对象的生命周期。
下面来看看一个完整的 Any 是如何实现的吧。
#include <iostream>
#include <string>
#include <memory>
#include <typeindex>
struct Any
{
Any(void) : m_tpIndex(std::type_index(typeid(void))) {}
Any(const Any &that) : m_ptr(that.Clone()), m_tpIndex(that.m_tpIndex) {}
Any(Any &&that) : m_ptr(std::move(that.m_ptr)), m_tpIndex(that.m_tpIndex) {}
// 创建智能指针时,对于一般的类型,通过 std::decay 来移除引用和 cv 符(即 const/volatile),从而获取原始类型
template <typename U, class = typename std::enable_if<!std::is_same<typename std::decay<U>::type, Any>::value, U>::type>
Any(U &&value) : m_ptr(new Derived<typename std::decay<U>::type>(forward<U>(value))),
m_tpIndex(type_index(typeid(typename std::decay<U>::type))) {}
bool IsNull() const { return !bool(m_ptr); }
/// @note 类型不相同
template <class U>
bool Is() const
{
return m_tpIndex == type_index(typeid(U));
}
// 将 Any 转换为实际的类型
template <class U>
U &AnyCast()
{
if (!Is<U>())
{
std::cout << "can not cast to " << typeid(U).name() << " from " << m_tpIndex.name() << std::endl;
throw std::bad_cast();
}
/// @note 将基类指针转为实际的派生类型
auto derived = dynamic_cast<Derived<U> *>(m_ptr.get());
return derived->m_value; ///< 获取原始数据
}
Any &operator=(const Any &a)
{
if (m_ptr == a.m_ptr)
return *this;
m_ptr = a.Clone();
m_tpIndex = a.m_tpIndex;
return *this;
}
private:
struct Base;
typedef std::unique_ptr<Base> BasePtr; ///< 基类指针类型
/// @note 基类不含模板参数
struct Base
{
virtual ~Base() {}
virtual BasePtr Clone() const = 0;
};
/// @note 派生类含有模板参数
template <typename T>
struct Derived : Base
{
template <typename U>
Derived(U &&value) : m_value(forward<U>(value)) {}
/// @note 将派生类对象赋值给了基类指针,通过基类擦除了派生类的原始数据类型
BasePtr Clone() const
{
return BasePtr(new Derived<T>(m_value)); ///< 用 unique_ptr 指针进行管理
}
T m_value;
};
BasePtr Clone() const
{
if (m_ptr != nullptr)
return m_ptr->Clone();
return nullptr;
}
BasePtr m_ptr;
std::type_index m_tpIndex;
};
测试代码:
void TestAny()
{
Any n;
auto r = n.IsNull(); // true
string s1 = "hello";
n = s1;
n = "world";
n.AnyCast<int>(); // 无法将 string 转为 int
Any n1 = 1;
n1.Is<int>(); // true
}
总结
再总结一下 Any 的设计思路:Any 内部维护了一个基类指针,通过基类指针擦除具体类型,AnyCast 时再通过向下转型获取实际数据。当转型失败时打印详情。