Reputation: 1053
Please consider the following code snippet:
#include <iostream>
template <typename T>
class SaveType {
public:
T* allocate() const { return new T; }
T* cast(void* obj) const { return static_cast<T*>(obj); }
};
int main() {
int i = 4;
// "save" the type of the object i in SType:
SaveType<decltype(i)> SType;
// do type erasure
void* z = static_cast<void*>(&i);
// do stuff with z ...
// undo type erasure only with the help of SType
decltype(SType.allocate()) h = SType.cast(z);
std::cout << *h << std::endl;
}
The above code compiles and runs fine as you can see online at Godbolt. But the code looks rather clumsy. Is there a better solution for undoing type erasure available in c++17 or c++20?
Upvotes: 3
Views: 1481
Reputation: 14577
Elegance aside, as mentioned in the comments your code snippet can be simplified as follows:
#include <iostream>
int main() {
int i = 4;
// "save" the type of the object i in SType:
using SType = decltype(i); // or with the older syntax: typedef decltype(i) SType;
// do type erasure
void* z = static_cast<void*>(&i);
// do stuff with z ...
// undo type erasure only with the help of SType
auto h = static_cast<SType*>(z); // or with the older less safe C-style syntax: auto h = (SType*)z;
std::cout << *h << std::endl;
}
#include <any>
#include <iostream>
int main() {
int i = 4;
// "save" the type of the object i in SType:
using SType = decltype(i);
// do type erasure
std::any z = i;
// do stuff with z ...
// undo type erasure only with the help of SType
auto h = std::any_cast<SType>(z);
std::cout << h << std::endl;
}
In both cases your SaveType
class was not used since it only works (without specifying types) in a local scope and is therefore redundant. To rectify this you'd have to implement @MichaelAaronSafyan's code snippet:
#include <iostream>
#include <memory>
class SaveType
{
public:
virtual ~SaveType(){}
virtual void* allocate()const=0;
virtual void* cast(void* obj)const=0;
};
template<typename T> class Type : public SaveType
{
public:
virtual void* allocate()const{ return new T; }
virtual void* cast(void* obj)const{ return static_cast<T*>(obj); }
};
int main() {
int i = 4;
// "save" the type of the object i in SType:
std::unique_ptr<SaveType> SType = std::make_unique<Type<int>>();;
// do type erasure
void* z = static_cast<void*>(&i);
// do stuff with z ...
// undo type erasure only with the help of SType
decltype(SType->allocate()) h = SType->cast(z);
std::cout << typeid(h).name() << std::endl;
// undo type erasure manually
auto h2 = *(int*) z;
std::cout << h2 << std::endl;
}
This allows you to store SaveType
in a container ahead of time and therefore use it in multiple scopes, however (as demonstrated above), it comes with its own problem in that it returns void*
instead of T*
(because the base class does not know what its derived class does).
To summarize (with a bonus):
If your implementation uses templates but does not account for scope you would not be able to access the type in non-local scopes because you would have to store it inside a container that knows something it cannot know.
If your implementation uses templates but accounts for scope (as demonstrated above) you would not be able to access the original type because you have to access it through a base class that knows something it cannot know.
Bonus: If your implementation uses std::type_info
, std::type_index
(C++11) or std::any::type
(C++17) you would be able to access the "type" but the type you access cannot be used for type casting.
Super Bonus: If your implementation uses a Covariant Return Type
you still would not be able to access the "type" because its implicit reconversion is superficial.
For implementation #1, you can only undo type erasure within the same context in which it was erased.
For implementation #2, (if applicable) you can forgo direct access and make it so the base class does not need to know what it cannot know thereby allowing the derived class to act on information only it knows. This is called the, "Tell, Don't Ask"
principle.
For implementation #3, all typeid
, decltype
(C++11), and std::any::type
(C++17) can help you do is expedite the process of referencing a pool of possible types (and unless you know for fact that this pool consists of a handful of specific types, I would not suggest writing the code by hand but instead programmatically generating it based on a likewise generated list of possible types).
For implementation #4, just consider this a dead end.
Upvotes: 5
Reputation: 137930
Funny you should ask this week, because I just happen to be reviewing a paper I wrote in 2015 on "undoing type erasure".
It proposes a generic interface for classes that wrap something like void*
, supporting modern C++ usage with const-correctness and lvalue/rvalue categories. You could give it a try if you are implementing your own type erasure.
As for erasure classes, they usually do a better job than your simple code, but it's tricky to get 100% right, as you found. Classes like any
, variant
, and function
usually get the job done, but with some rough edges. That's what motivated me to write the paper, but motivating others to adopt it is another matter!
Upvotes: 2