Type erasure
type erasure这个术语在c++中很少被提及,但是它所代表的技术却被非常广泛的应用。很多人听到这个词的第一印象可能是多态,通过继承与虚函数override
来实现。但其实当我们在c++中聊到type erasure,他更多的是表示通过模板和继承来共同实现的。std::any
就是一个例子。不过这篇文章我们介绍一下所有和type erasure概念有关的技术。
基于void*
void*
几乎可以说是任何类型的alias,所以当我们用这个的时候大部分就是将某个类型转换成void*
然后传递。比如
void* bsearch (const void *key,
const void *base,
size_t nmemb,
size_t size,
int (*compar)(const void*, const void*));
为了让他能处理多种类型,我们把参数射程了void*
. 这样类型就被我们擦除了,但问题是这样会造成类型安全不能保证,当你key
和compar
所接受的类型不一样时,就会在运行时出现Undefined Behavior错误,看如下例子:
int compareChar(const void*a , const void* b)
{
return -strcmp ((const char *)a, (const char *)b);
}
bool findInVector(std::vector<int>& vec, int val)
{
bsearch(&val, vec.data(), vec.size(), sizeof(int), compareChar);
}
基于Obeject oriented继承
比如我们有如下代码:
struct Circle : public Shape
{
public:
void draw() override
{
//...impl
}
};
void drawAShape(Shape* s);
通过继承自一个基类,然后在接口里传入基类的指针。 这个和void*
比起来的好处是不管真正被调用的是什么类型,他一定有相应的函数可以被调用,所以不会出现UB,也保障了类型安全。但我感觉这算不上真正的类型擦除,因为他只是擦除了调用处的对象类型。他的问题在于要求每个实现类型必须继承自某一个基类,很多时候过多的继承关系会有耦合问题或者其他设计问题。
template
比如std::function
,
std::function<bool(int, int)> predicate;
,他对于type的要求是非侵入性的,即duck-typing-like(如果一个东西走路像鸭子,叫的像鸭子,那他就是鸭子)。从std::function
的例子我们可以看到,它对类型的唯一要求就是含有bool operator()(int, int)
。
继承与temnplate的结合
std::any(c++17)就是一个很好的例子,它允许你通过any这个类来保存任何类型对象,而且不需要在构建时指定模板参数。
std::any a = 1;
std::cout << std::any_cast<int>(a) << std::endl; // output 1
a = 3.14;
std::cout << std::any_cast<double>(a) << std::endl; // output 3.1
std::cout << a.is<int>(); //output false
因为不能接受模板参数,我们需要在内部含有一个类模板来实现构造时的类型推导。但是如果仅仅有这个类模板,我们发现还是要从any
外面传入模板参数才能写出构造函数,这样与我们的初衷不符。所以我们需要一个基类来作为any
的成员变量,然后让一个类模板继承自这个基类来完成类型推导。基类会作为接口,其内部需要定义我们我们需要的方法的虚函数.由于我们会在any
类内部创建一个以Base
为基类的Derived
对象,所以我们可以用unique_ptr
来管理生命周期
struct any
{
template <typename U>
any(U&& value) : m_ptr(new Derived<typename std::decay<U>::type>(std::forward<U>(value))){}
template <typename U>
any& operator=(const any& rhs)
{
if(m_ptr == rhs.m_ptr)
return *this;
m_ptr = std::unique_ptr<Base>(new Derived<U>(rhs.m_ptr->m_value));
}
struct Base
{
virtual ~Base(){}
};
template<typename T>
struct Derived : Base
{
template <typename U>
explicit Derived(U&& value) : m_value(std::forward<U>(value)){}
T m_value;
};
std::unique_ptr<Base> m_ptr;
};
接下来我们实现is
和any_cast
.我们可以利用dynamic_cast
的特性去做检测,即如果不能被安全的cast成一个派生类,它会返回nullptr
。当然我们也可以选择在any
类里存储std::type_index
信息,这样在性能上应该可以更好。
struct any
{
// .....省略上面写过的代码
template<class U>
bool is() const
{
return dynamic_cast<Derived<U>*>(m_ptr.get()) != nullptr;
}
template <class U>
U& any_cast()
{
auto d = dynamic_cast<Derived<U>*>(m_ptr.get());
if(d == nullptr)
{
throw bad_cast();
}
return d->m_value;
}
};
以上大概是std::any
的一个简陋实现,Derived
通过模板来存储了真实类型,而Base
作为含有接口的抽象类在any
被赋值时将类型进行擦除。这种技术在平时工作中的使用场景非常多,比如说你有如下需求:
- 用同一种方式处理不同类型
- 用同一种类型容器保存不同类型对象