Just when you thought having unambiguous types was the one true way of Swift, it turns out that sometimes it is necessary to erase types. In this talk from try! Swift, Gwendolyn Weston explains what a type is, what it means to erase a type, and why you would want to do it.
In this great talk from try! Swift Gwendolyn Weston introduces you to the concept of type erasure.
Type erasure can be a very powerful tool when used correctly and is even present in the Swift Standard Library.
Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
✓ Live Streaming✓ Interactive Chat✓ Private Shows✓ HD Quality✓ Free Actions
Free to watch • No registration required • HD streaming
One thing that a lot of higher level OO languages (higher than C++) have going for them is having a single base class to rule them all. I am talking about 'object'. In C#, this class can represent any object in the program, and also allows you to get a string version of this object.
C++ on the other hand has no such construct. This is both good and bad. On the good side, it falls in line with C++'s tenet of "pay for only what you use". Forcing all objects to share a single base class causes 2 things to become hard:
C interop becomes hard because you no longer have PODs
Every class is now virtual and has a v-table.
i) The destructor is virtual so cleanup is no longer trivial for RAII patterns
ii) The v-table takes up an extra pointers worth of memory, so data size compression becomes trickier
For those reasons, I love that C++ doesn't enforce such a base class. However, it does make things much trickier when you just need some data to be carried through the system in a transparent way, until someone later down the road can unpack it and use it. For this reason, you often see "void *" getting thrown around a lot (especially in C api's). I am going to be so bold as to say "void *" sucks! It sucks for so many reasons that I should probably create another post for it.
In this post, I would like to talk about an implementation in C++ that allows you to store any data type transparently, while still preserving type safety. Before you blast me and say boost::any already does this, my container also supports polymorphism.
Here are the goals for this container:
It should be reference counted, so that the data can be shared amongst any number of owners safely.
It must allow any data type, be it int or Foo.
The container must be type safe, so that you can't get a handle to invalid or corrupt data
The container must allow polymorphism to work properly. What I mean by this is that someone should be able to put a derived class into the container, and request it back as the base class.
The container should provide simple mechanisms for getting data into the container, be it a pointer, or a copyable data type. If the type is a pointer, the simplest method should also take ownership of the pointer and call delete when the data is no longer used.
To implement this container, I am going to use a technique called Type Erasure. This essentially means taking a bunch of classes with a common interface and providing access to them all through a single interface. Here is the base interface for the container:
class GenericData { public: typedef boost::shared_ptr<GenericData> Ptr; virtual ~GenericData() { } template<typename T> T &GetData() const { try { // GetDataInternal will return a void* to the stored data object. // If typeid(T).hash_code() is not equal to the hash code of the stored type, // an exception is thrown that contains the name of the stored type for the exception message return *(T*)GetDataInternal(typeid(T).hash_code()); } // The types are not compatible catch (std::exception &ex) { auto fmt = boost::format("Unable to retrieve generic data. The requested data type was '%1%', but the stored type is '%2%'") % typeid(T).name() % ex.what(); throw std::exception(fmt.str().c_str()); } } template<typename T> T GetDataDyn() const { try { // The following function will throw the actual stored data. // If 'T' is the stored type, or is a base class for it, the catch handler will // trigger and we have a pointer to T at this point. It is a cool trick that abuses // the exception system, but is otherwise necessary. ThrowDataInternal(); throw std::exception("Unreachable"); } // verify_pointer is a class that prevents the use of non-pointers // This is important because an exception handler will clean up the argument to the handler, // and if the argument is not a pointer, it will have its destructor called // // 'type' is simply T, but T MUST be a pointer catch (const verify_pointer<T>::type &data) { return const_cast<T>(data); } catch (...) { // 'T' does not live in the same inheritance hierarchy as the stored type } return nullptr; } protected: // This method will return a pointer to the stored data. // If typeHash does not match the hash code for the stored data type, an exception is thrown virtual void *GetDataInternal(size_t typeHash) const = 0; virtual void ThrowDataInternal() const = 0; // Returns the name of the class of the stored type virtual const char *GetName() const = 0; };
That is it for the base class. I did omit some of the functions for brevity.
The "GetData<T>" function is what you would use if your class is not polymorphic, or you know exactly what type is stored in the container. This function will throw an exception if T doesn't exactly match the type of data stored in the container.
The "GetDataDyn<T>" function allows you to access the stored data using any T that is within the inheritance hierarchy of the stored type. This function enforces that T is a pointer type.
The next step is the implementation:
template<typename DataT> class GenericDataImpl : public GenericData { public: GenericDataImpl(const DataT &data) : _data(data) { } const DataT &Data() const { return _data; } protected: virtual void * GetDataInternal( size_t typeHash ) const { // Get the hash code for the stored type static const size_t DATA_HASH_CODE = typeid(DataT).hash_code(); // Check to make sure that the stored type hash code matches the hash for the requested type (i.e. they are the same type) if (typeHash != DATA_HASH_CODE) throw std::exception(GetName()); // const_cast isn't technically necessary, however the returned type is 'const DataT *' and the requested type is 'DataT *' so this just keeps them the same return const_cast<DataT*>(&_data); } virtual void ThrowDataInternal() const { throw _data; } virtual const char *GetName() const { return typeid(DataT).name(); } private: DataT _data; };
This class is relatively self explanatory. The reason we are doing a const_cast is because the scope of the function is const, so we would technically be returning a "const DataT *" but I don't like void * voodoo, so I want the types to be the same on both sides of the wormhole.
Finally, we just have a few holes to fill before you have the full implementation (I will post the source so that you have that if you are lazy):
template<typename T> class DefaultDeleter { public: void operator()(const T &val) const { // This is a no op because T is not a pointer, and therefore is automatically destructed } }; template<typename T> class DefaultDeleter<T*> { public: void operator()(T *val) const { // Delete the pointer. This will fail for arrays, so don't use it for arrays delete val; } };
Similarly, we will use the partial specialization trick to enforce that T is a pointer for the GetDataDyn class.
// These structs are here to ensure that no one tries to access GetDataPoly with a type 'T' that is not a pointer type. // This is because the cleanup for the exception block will actually call the destructor on our type, which is not good template<typename T> struct verify_pointer { static_assert(sizeof(T) == 0, "Using generic data polymorphism requires the data be a pointer, however one was not provided"); typedef T type; }; template<typename T> struct verify_pointer<T*> { typedef T* type; };
Since we are using verify_pointer<T>::type as the exception handler argument, we are forcing the compiler to decide whether or not T is a pointer, and if not, the static_assert will trigger and fail the compile.
Finally, we just have the creation functions:
// Constructs a generic data using the specified data, and the specified deleting functor. The functor needs the "operator()(const DataT &val) const" operator. template<typename DataT, typename DeleterT> IGenericData::Ptr make_data(const DataT &data, const DeleterT &del, bool shouldTransfer = false) { return IGenericData::Ptr(new GenericDataImpl<DataT>(data, shouldTransfer), [del](GenericDataImpl<DataT> *data) { del(data->Data()); delete data; }); } // Constructs a generic data using the default lifetime for the supplied object. // This means that the destructor for the data item will get called when the GenericData shared pointer gets freed. This works for pointers and regular items. This does, however, corrupt your heap if you use it on an array. template<typename DataT> IGenericData::Ptr make_data(const DataT &data, bool shouldTransfer = false) { return make_data(data, DefaultDeleter<DataT>(), shouldTransfer); } template<typename DataT> IGenericData::Ptr make_array_data(DataT *arrData, bool shouldTransfer = false) { // Use a lambda that uses the delete[] which works for arrays auto del = [](const DataT *&d) { delete[] d; d = nullptr; }; return make_data(arrData, del, shouldTransfer); } template<typename DataT> IGenericData::Ptr make_no_delete_data(const DataT &data, bool shouldTransfer = false) { // Using this construction method, the lifetime of the data object is outside of the data container, and must be managed externally. auto del = [](const DataT &d) { }; return make_data(data, del, shouldTransfer); }
That is all. If you have questions, feel free to ask. I can also provide the actual source if you need me to.