https://blog.the-pans.com/type-erasure/
C++ Type Erasure,C++类型消除,是一种常用的范式,用于在C++中实现类型安全的类Duck Typing。
考虑一下下面的接口和问题:
class Shelf {
...
public:
void forEachBook(??? f); // 这里的 ??? 应该怎么定义才能让用户完成如下调用?
...
}
shelf.forEachBook([](auto&& book){ book.print(); });
以及:
std::vector<???> printables; // ??? is some type that defines operator()
for (auto printable : printables) {
printable();
}
模板
可以将forEachBook定义成模板方法:
template<typename T>
void forEachBook(T& f);
但是,这样做有几个模板导致的问题:
- 这个方法实现必须写在头文件里(所有模板方法实现都是如此);
- 调用方如果使用多个lambda调用该方法,每一个lambda定义都会导致方法被模板特化一次;
抽象基类
另一个思路是,可以让forEachBook接受一个抽象基类,调用方需要实现子类并传入:
class AbstractFoo {
virtual void operator()(Book&& book) = 0;
virtual ~AbstractFoo();
}
void forEachBook(AbstractFoo& f);
但是,这样做也有以下问题:
- 破坏了duck typing模式,lambda并不继承自任何基类,无法直接将lambda传入该方法;
- 此外,可能有部分已有代码是满足duck typing,但没有上述继承关系的,因此,可能需要自己封装adapter;
Type Erasing
class AbstractFoo {
public:
virtual void operator()(Book &&book) = 0;
virtual ~AbstractFoo();
}
template<typename T>
class AbstractFooWrapper : AbstractFoo {
public:
AbstractFooWrapper(t): t(t) {}
void operator()(Book &&book) {t(book);}
private:
T t;
}
class FinalFoo {
public:
template<typaname T>
FinalFoo(T t) { wrapper.reset(new AbstractFooWrapper<T>(t));}
void operator()(Book &&book) {wrapper->operator()(book);}
private:
std::unique_ptr<AbstractFoo> wrapper;
}
void forEachBook(FinalFoo& f);
使用上面的写法:
- FinalFoo本身并不会成为一个template class,只有它的Copy Constructor是一个template function,因此用多种类型创建FinalFoo只会导致Copy Constructor被生成多份;
- FinalFoo可以包装任意的类型T,并且由于模板实现代码的限制,T必须是一个接受Book&&作为参数的可调用对象,调用FinalFoo的operator()就会调用T;
- 类似的,我们还可以在FinalFoo中添加更多的duck-typing约束,只要像上面一样:
- 在AbstractFoo中声明virtual type-typeing约束方法;
- 在AbstractFooWrapper中利用模板类型变量t实现type-typing方法;
- 在FinalFoo的Copy Constructor中接受模板类型,并定义type-typing方法,利用指针调用AbstractFoo实现。
TypeErasing的其他应用
std::function
std::function用到了type erasure,下面是一个 over simplified 示例:
template<typename R, typename... Args>
struct callable_base {
virtual R operator()(Args...) = 0;
virtual ~callable_base() {}
};
template<typename F, typename R, typename... Args>
struct callable : callable_base<R, Args...> {
F functor;
callable(F functor) : functor(functor) {}
virtual R operator()(Args... args) { return functor(args...); }
};
template <typename T> class function;
template <typename R, typename... Args>
class function<R(Args...)> {
std::unique_ptr<callable_base<R, Args...>> c;
public:
template <typename F>
function(F f) {
c.reset(new callable<F, R, Args...>(f));
}
R operator()(Args... args) { return (*c)(args...); }
// ...
};
int returnInt() {
return 2;
}
auto lambda = []() -> int {
return 3;
};
function<int()> f1(lambda);
function<int()> f2(returnInt);