What Is Type Erasure in C++

What is type erasure in C++?

Here's a very simple example of type erasure in action:

// Type erasure side of things

class TypeErasedHolder
{
struct TypeKeeperBase
{
virtual ~TypeKeeperBase() {}
};

template <class ErasedType>
struct TypeKeeper : TypeKeeperBase
{
ErasedType storedObject;

TypeKeeper(ErasedType&& object) : storedObject(std::move(object)) {}
};

std::unique_ptr<TypeKeeperBase> held;

public:
template <class ErasedType>
TypeErasedHolder(ErasedType objectToStore) : held(new TypeKeeper<ErasedType>(std::move(objectToStore)))
{}
};

// Client code side of things

struct A
{
~A() { std::cout << "Destroyed an A\n"; }
};

struct B
{
~B() { std::cout << "Destroyed a B\n"; }
};

int main()
{
TypeErasedHolder holders[] = { A(), A(), B(), A() };
}

[Live example]

As you can see, TypeErasedHolder can store objects of an arbitrary type, and destruct them correctly. The important point is that it does not impose any restrictions on the types supported(1): they don't have to derive from a common base, for example.


(1) Except for being movable, of course.

Is type erasure concept exist in C-language?

Are you a Java developer?

If yes you need to forget a LOT of things and learn another LOT.

Down casting means inheritance. So this is not C.

C can cast almost anything to anything (C assumes that you know what you are doing, contrary to Java, C# and others)

Type erasure is a Java concept because JRE doesn't have generics at run-time, only raw-types.

In C you have only binary code at run-time, just like in Assembly and that's all we need to have lightning-fast executables (remember that almost any compiler, framework or virtual machine is built in C or C++).

In C there is no such thing of RTTI (Run-Time Type Information) and we don't need this because the aim of C is program directly over the metal.

Template type erasure

Doing this with a compile-time check is, unfortunately, not feasible. You can, however, provide that functionality with a runtime check.

A map's value type can only be one single type, and Foo<T> is a different type for each T. However, we can work around this by giving every Foo<T> a common base class, have a map of pointers to it, and use a virtual function to dispatch call() to the appropriate subclass.

For this though, the type of the argument must also always be the same. As mentioned by @MSalters, std::any can help with that.

Finally, we can wrap all that using the pimpl pattern so that it looks like there's just a single neat Foo type:

#include <cassert>
#include <string>
#include <functional>
#include <any>
#include <unordered_map>
#include <memory>

struct Foo {
public:
template<typename T, typename FunT>
void set(FunT fun) {
pimpl_ = std::make_unique<FooImpl<T, FunT>>(std::move(fun));
}

// Using operator()() instead of call() makes this a functor, which
// is a little more flexible.
void operator()(const std::any& arg) {
assert(pimpl_);
pimpl_->call(arg);
}

private:
struct IFooImpl {
virtual ~IFooImpl() = default;
virtual void call( const std::any& arg ) const = 0;
};

template <class Arg, typename FunT>
struct FooImpl : IFooImpl
{
FooImpl(FunT fun) : fun_(std::move(fun)) {}

void call( const std::any& arg ) const override {
fun_(std::any_cast<Arg>(arg));
}

private:
FunT fun_;
};

std::unique_ptr<IFooImpl> pimpl_;
};

// Usage sample
#include <iostream>

void bar(int v) {
std::cout << "bar called with: " << v << "\n";
}

int main() {
std::unordered_map<std::string, Foo> table;

table["aaa"].set<int>(bar);

// Even works with templates/generic lambdas!
table["bbb"].set<float>([](auto x) {
std::cout << "bbb called with " << x << "\n";
});

table["aaa"](14);
table["bbb"](12.0f);
}

see on godbolt

Type erasure for methods with differing in return types

Type erasure can and has been implemented in C++ in different contexts. The most common approach, which is used in boost::any, std::function< signature >, std::thread and others is based on a non-polymorphic class that is the type erased object, which contains a pointer to an interface type. Internally, during construction, assignment or whenever the user type is erased, an implementation of the interface is instantiated and stored.

As a motivating simplified example, consider that we wanted to create a printable type that can be used to print any type that implements operator<< to std::cout using type erasure. For that we need the type printable, the internal interface printable_impl_base, and the actual implementations:

// regular polymorphic hierarchy:
struct printable_impl_base {
virtual ~printable_impl_base() {}
virtual void print() const = 0;
};
template <typename T>
struct printable_impl : printable_impl_base {
T copy_to_print;
printable_impl( T const & o ) : copy_to_print( o ) {}
virtual void print() const {
std::cout << copy_to_print << std::endl;
}
};

// type erasure is performed in printable:
class printable {
std::shared_ptr<printablable_impl_base> p;
public:
template <typename T>
printable( T obj ) : p( new printable_impl<T>(obj) ) {}
void print() const {
p->print();
}
};

Note that the pattern is very similar to a regular polymorphic hierarchy, with the difference that an interface object is added that is a value type (borrowing the term value type from C#), that holds the actual polymorphic objects inside.

Looking at it this way, it seems kind of simplistic and useless, but that is the fuel that drives boost::any (the internal interface is only a typeid), std::function< void () > (the internal interface is that it implements void operator()), or shared_ptr<> (the interface is the deleter method, that relinquishes the resource).

There is one specific different type of type erasure when the only thing that needs to be done with the type that implements type erasure is to destroy it: use a temporary and bind it to a constant reference... But this is very specific, if you want you can read about it here: http://drdobbs.com/cpp/184403758

In the specific case that you are talking about in the question it is a bit more complex, because you don't want to erase a single type, but rather a couple of them. The Iterable interface must erase the type of the container that it internally holds, and in doing so it has to provide it's own iterators that have to perform type erasure on the iterators from the container. Still, the idea is basically the same, just more work to do to implement.

Type erasing type erasure, `any` questions?

Here's my solution. It looks shorter than Yakk's, and it does not use std::aligned_storage and placement new. It additionally supports stateful and local functors (which implies that it might never be possible to write super_any<&print>, since print could be a local variable).

any_method:

template<class F, class Sig> struct any_method;

template<class F, class Ret, class... Args> struct any_method<F,Ret(Args...)> {
F f;
template<class T>
static Ret invoker(any_method& self, boost::any& data, Args... args) {
return self.f(boost::any_cast<T&>(data), std::forward<Args>(args)...);
}
using invoker_type = Ret (any_method&, boost::any&, Args...);
};

make_any_method:

template<class Sig, class F>
any_method<std::decay_t<F>,Sig> make_any_method(F&& f) {
return { std::forward<F>(f) };
}

super_any:

template<class...OperationsToTypeErase>
struct super_any {
boost::any data;
std::tuple<typename OperationsToTypeErase::invoker_type*...> operations = {};

template<class T, class ContainedType = std::decay_t<T>>
super_any(T&& t)
: data(std::forward<T>(t))
, operations((OperationsToTypeErase::template invoker<ContainedType>)...)
{}

template<class T, class ContainedType = std::decay_t<T>>
super_any& operator=(T&& t) {
data = std::forward<T>(t);
operations = { (OperationsToTypeErase::template invoker<ContainedType>)... };
return *this;
}
};

operator->*:

template<class...Ops, class F, class Sig,
// SFINAE filter that an op matches:
std::enable_if_t< std::disjunction< std::is_same<Ops, any_method<F,Sig>>... >{}, int> = 0
>
auto operator->*( super_any<Ops...>& a, any_method<F,Sig> f) {
auto fptr = std::get<typename any_method<F,Sig>::invoker_type*>(a.operations);
return [fptr,f, &a](auto&&... args) mutable {
return fptr(f, a.data, std::forward<decltype(args)>(args)...);
};
}

Usage:

#include <iostream>
auto print = make_any_method<void(std::ostream&)>(
[](auto&& self, auto&& os){ os << self; }
);

using printable_any = super_any<decltype(print)>;

printable_any bob = 7; // sets up the printing data attached to the any

int main() {
(bob->*print)(std::cout); // prints 7
bob = 3.14159;
(bob->*print)(std::cout); // prints 3.14159
}

Live

Type erasure: Retrieving value - type check at compile time

For a limited set of types, your best option is variant. You can operate on a variant most easily by specifying what action you would take for every single variant, and then it can operate on a variant correctly. Something along these lines:

std::unordered_map<std::string, std::variant<Foo, Bar>> m;

m["a_foo"] = Foo{};
m["a_bar"] = Bar{};

for (auto& e : m) {
std::visit(overloaded([] (Foo&) { std::cerr << "a foo\n"; }
[] (Bar&) { std::cerr << "a bar\n"; },
e.second);
}

std::variant is c++17 but is often available in the experimental namespace beforehand, you can also use the version from boost. See here for the definition of overloaded: http://en.cppreference.com/w/cpp/utility/variant/visit (just a small utility the standard library unfortunately doesn't provide).

Of course, if you are expecting that a certain key maps to a particular type, and want to throw an error if it doesn't, well, there is no way to handle that at compile time still. But this does let you write visitors that do the thing you want for each type in the variant, similar to a virtual in a sense but without needing to actually have a common interface or base class.

what is the logic of type erasure in java generics?

To complement @Giorgi Tsiklauri's answer,

For the class below:

class Box<T> {
T t;

T getT() { return t; }

void setT(T t) { this.t = t; }
}

because of type erasure, compiler compiles the class to:

class Box {
Object t;

Object getT() { return t; }

void setT(Object t) { this.t = t; }
}

So, when you instantiate the Box<T> generic class with type argument Integer as follows:

Box<Integer> box = new Box<Integer>();
box.setT(10);
Integer t = box.getT();

the compiler knows that the Box<T> is instantiated with Integer because, at compile time, it sees that Box<Integer>. So, type casts are inserted by compiler. So the code above is compiled into:

Box box = new Box();
box.setT(10);
Integer t = (Integer) box.getT(); // compiler inserts type casts in lieu of us!

No matter how you instantiate the generic, the compiler doesn't create additional class files. If what actually happens at type erasure is replacement(replaces T with type argument such as Integer), the compiler have to create additional classes for Box for Integer, Box for Double etc — But that's not what actually happens at type erasure:

Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. — The Java™ Tutorials

Insert type casts if necessary to preserve type safety. — The Java™ Tutorials

Type erasure ensures that no new classes are created for parameterized types; consequently, generics incur no runtime overhead. — The Java™ Tutorials



Related Topics



Leave a reply



Submit