Confused about Type Erasure in Hands-On Design Patterns with C++ by Fedor G. Pikus

Viewed 48

In his book, Fedor G. Pikus talks about type erasure in C++. Specifically, he mentions the example of a smart pointer which has its own deleter whose type is "abstracted away".

This is the example he gives:

template <typename T>
class smartptr {

    struct deleter_base {
        virtual void apply(void*) = 0;
        virtual ~deleter_base() { }
    };

    template <typename Deleter>
    struct deleter : public deleter_base {
        deleter (Deleter d) : d_(d) { }
        virtual void apply(void* p) { d_(static_cast<T*>(p)); }
        Deleter d_;
    };

    public:
    template <typename Deleter>
        smartptr(T* p, Deleter d) : p_(p), d_(new deleter<Deleter>(d)) {}
    ~smartptr() { d_->apply(p_); delete d_; }
    T* operator->() { return p_; }
    const T* operator->() const { return p_; }

    private:
    T* p_;
    deleter_base* d_;
};

While I understand the implementation for the most part, there is one small thing I can't seem to figure out.

Why do the virtual apply methods of the deleter_base and deleter class use void* pointers and then static_cast to a pointer of type T* instead of just being of type T* to begin with?

As so:

...
    struct deleter_base {
        virtual void apply(T*) = 0;
        virtual ~deleter_base() { }
    };

    template <typename Deleter>
    struct deleter : public deleter_base {
        deleter (Deleter d) : d_(d) { }
        virtual void apply(T* p) { d_(p); }
        Deleter d_;
    };
...
1 Answers

Why do the virtual apply methods of the deleter_base and deleter class use void* pointers and then static_cast to a pointer of type T* instead of just being of type T* to begin with?

The answer to your question is simple: taking this example code in isolation, there is no particular reason to use the void* and static_cast<T*> approach. The code below, with the edits you suggest to replace void* with T*, works just fine.

I prefer to spell it out as T* because it makes the code more clear. It is important to note that the void* approach is still correct -- it is always legal to cast a pointer to void* and back again.

Using T* works because the inner deleter_base class is still a template type parameterized on T. In other words, it is a smartptr<T>::deleter_base and so has full access to T. In fact, no smartptr<T> will ever construct a deleter_base that passes anything but a T* to its apply method.

Using void* is a classic C-style approach to type erasure, so the author may have used it out of habit. Or perhaps an earlier design required void* and it stayed that way after some revision.

For example, Igor Tandetnik raised the possibility that void* leaves room to convert a smartptr<Derived> into a smartptr<Base> while still using the same deleter object. But, to do this the T type must be erased from deleter_base as well, which is not the case here (because it is an inner class of the smartptr<T> template).

However, the apparent point here is to "type erase away" the Deleter type from the smartptr<T>, and not require the user to spell the type smartptr<T, Deleter>. Both approaches under discussion here accomplish this goal.

#include <memory>

template <typename T>
class smartptr {
    struct deleter_base {
        virtual void apply(T*) = 0;
        virtual ~deleter_base() {}
    };

    template <typename Deleter>
    struct deleter : public deleter_base {
        deleter(Deleter d) : d_(d) {}
        virtual void apply(T* p) { d_(p); }
        Deleter d_;
    };

   public:
    template <typename Deleter>
    smartptr(T* p, Deleter d) : p_(p), d_(new deleter<Deleter>(d)) {}
    ~smartptr() {
        d_->apply(p_);
        delete d_;
    }
    T* operator->() { return p_; }
    const T* operator->() const { return p_; }

   private:
    T* p_;
    deleter_base* d_;
};

void Code() {
    smartptr<int> foo(new int, std::default_delete<int>());
}
Related