Type Erasure

Type erasure allows the creation of a gneneric container, i.e., one that contains instances of different types. This contravenes C++’s normal requirements of strict and static typing. This pattern is attributed to Kevlin Henney, C++ Report 12(7), July/August 2000, entitled, Valued Conversions. His definition of the any type follows:

class any
{
public:

  any() : content(0) {}

  ~any() { delete content; }

  const std::type_info & type_info() const {
    return content ? content->type_info() : typeid(void);
  } // type_info

private:

  struct placeholder
  {
    virtual ~placeholder() {}

    virtual const std::type_info &
    type_info() const = 0;

    virtual placeholder * clone() const = 0;

  }; // struct placeholder

  // This is the main 'trick': We generate a type that derives from the
  // placeholder that wraps the user type. This type is used internally
  // to store the instances of the user types. This could be done
  // externally by defining a common base type. However, this approach is
  // automatic and more convenient...
  template<typename value_type>
  struct holder : public placeholder
  {
    holder(const value_type & value) : held(value) {}

    virtual const std::type_info & type_info() const {
      return typeid(value_type);
    } // type_info

    virtual placeholder * clone() const {
      return new holder(held);
    } // clone

    const value_type held;
  }; // struct holder

  placeholder * content;

}; // class any

Using this basic definition, we can add inward conversion interface from arbitrary types:

class any
{
public:

  // Copy constructor from another any instance.
  any(const any & other)
  : content(other.content ? other.content->clone() : 0)
  {
  }

  // Copy constructor from another instance of the same type.
  template<typename value_type>
  any(const value_type & value)
  : content(new holder<value_type>(value)) {}

  // Swap the contents
  any &swap(any & rhs)
  {
    std::swap(content, rhs.content);
    return *this;
  } // swap

  // Assignment to another any instance.
  any &operator=(const any & rhs)
  {
    return swap(any(rhs));
  } // operator =

  // Assignment to another instance of the same type.
  template<typename value_type>
  any &operator=(const value_type & rhs)
  {
    return swap(any(rhs));
  }
}; // class any

Recovering the typed value can be handled like:

class any
{
public:

  // Conversion to void *.
  operator const void *() const
  {
    return content;
  } // operator const void *

  // Conversion back to stored type.
  template<typename value_type>
  bool copy_to(value_type &value) const
  {
    const value_type * copyable =
    to_ptr<value_type>();
    if(copyable)
    value = * copyable;
    return copyable;
  } // copy_to

  // Conversion to pointer to stored type.
  template<typename value_type>
  const value_type * to_ptr() const
  {
    return type_info() == typeid(value_type)
      ? &static_cast<
        holder<value_type> *>(content)->held
      : 0;
  } // to_ptr

}; // class any

// Convenience cast function.
template<typename value_type>
value_type any_cast(const any &operand)
{
  const value_type * result =
  operand.to_ptr<value_type>();
  return result ? * result : throw std::bad_cast();
}

Working off of this model, FleCSI uses a simpler, less-explicit form of type erasure through the definition of a common method interface for a set of parameterized types. For example, if several types define a method erasure_method like

template<
  typename ... PARAMS
>
struct type_t
{

  // This method can be used to capture static parameters that were
  // passed to define the type. Runtime invocation of this method
  // allows the recovery of this type information.
  static
  void
  erasure_method()
  {
    // use PARAMS to do type-specific operations...
  } // erasure_method

}; // struct type_t

we can recover the type information at runtime by invoking the common method. References to each type’s erasure_method function can be stored in a standard container because they are all of the same type. This pattern is used in several places in FleCSI. A specific example is in flecsi/execution/legion/task_wrapper.h. In particular, task_wrapper::registration_callback and task_wrapper::execute_user_task use this design pattern.