Excellant videos:
- C++ Weekly – Ep 343 – Digging Into Type Erasure, Jason Turner
- Breaking Dependencies – C++ Type Erasure – The Implementation Details, Klaus Iglberger, CppCon 2022
Basic ideas in the examples:
- Create a class W to wrap the concrete types.
- W constructor captures concrete type information.
- W has templated constructor, so the constructor has the knowledge of the concrete type T. This information is used to perform operations that need/depend on T, while still in the W constructor. Approaches to capture the type information:
- Approach 1: Create an internal base class B, and a class template parameterized by the concrete type but deriving from the base, D<T>:B.
- B has virtual methods to dictate the oprations;
- D implements the operations with type information of T.
- W creates a new D object, e.g., moving from
tinW::W(T t). It stores a pointer (e.g.std::unique_ptr<B>) as data member.- W operations forward to B operations using the pointer member above.
- If it is too expensive even moving T, W may create a non-owing reference to
tinW::W(T& t), which can be captured in B and/or D. - If you need to capture other information to customize W, use extra parameters in
W::W(), either as template parameter or function parameter, for compile time or run-time customized behavior. An example is to pass a lambda to customize the operation. - If dynamic allocation to create D is too expensive, W can use use small buffer optimization (SBO) to move T over directly if it is a small type. It’s even possible to use a storage policy template parameter to customize W behavior, e.g. to always use dynamic allocation, to always use SBO, or to use SBO with fallback to dynamic allocation.
- Approach 2: W Constructor can create a lambda L and store it as data member, whose body uses the T information, e.g., calling a function depending on T.
- Approach 1: Create an internal base class B, and a class template parameterized by the concrete type but deriving from the base, D<T>:B.
- W has templated constructor, so the constructor has the knowledge of the concrete type T. This information is used to perform operations that need/depend on T, while still in the W constructor. Approaches to capture the type information:
- After the construction, W no long knows the concrete T type (type erased). It is a plain wrapper class object in appearance, and you cannot ask what’s the concrete it wraps.
- Operations on the W object rely on the internal B member or L member to perform.
- D or L‘s operations can connect to T in different ways:
- External polymorphism: D/L assumes a free function override that takes T object, the affordance function. As long as someone writes such the affordance, W would work with T, otherwise it would have a compile error. Note that even if you are not able to modify T, you can still make use of W by creating the affordance override.
- D/L assumes T has a specific method to perform an action. This is not always possible if you don’t control T.
A beautiful type erasure can represent the unrelated types with value semantics and have high performance.
Note that type erasure must use indirection (to hide “type”), which has similar performance loss like vtable, compared to class or function template that exposes type or implementation details.
Maybe we can create utitily class template for type erasure supporting:
- Concrete type object access
- Referencing (non-owning)
- Moving
- Copying
- Concrete type operation access (e.g. using C++20 concept)
- External polymorphism / free affordance function override; and/or
- requiring specific member function signature