Search code examples
c++templatesgeneric-programmingtype-erasure

Is there a way to simultaneously assign a type to multiple templates in C++?


This question is based on the example code below, which is inspired by Sean Parent's talk. The goal of the code below is to provide an object wrapper similar to boost::any. I wrote this code to educate myself of type erasure. So, there is no practical uses this code intends (considering there is already boost::any).

class ObjWrap {
public:
  template <typename T>
  ObjWrap(T O) : Self(new Obj<T>(std::move(O))) {}

  template <typename T>
  friend typename T * getObjPtr(ObjWrap O) {
    return static_cast<T*>(O.Self->getObjPtr_());
  }
private:
  struct Concept {
    virtual ~Concept() = 0;
    virtual void* getObjPtr_() = 0;
  };
  template <typename T>
  struct Obj : Concept {
    Obj(T O) : Data(std::move(O)) {}
    void* getObjPtr_() { return static_cast<void*>(&Data); }

    T Data;
  };

  std::unique_ptr<Concept> Self;
};

Before I can really ask my question, let's examine the code in the following aspects:

  1. Concept::getObjPtr_ returns void* because a) Concept cannot be a template otherwise unique_ptr<Concept> Self would not work; b) void* is the only way I know how to return Obj::Data in a type-agnostic way in C++. Please correct me if this is wrong...

  2. T * getObjPtr(ObjWrap O) is a template that needs instantiation separately from the ObjWrap constructor.

  3. The use of ObjWrap basically includes: a) make a new ObjWrap over an existing object; b) retrieve the underlying object given an ObjWrap. For example:

    ObjWrap a(1);
    ObjWrap b(std::string("b"));
    int* p_a = getObjPtr<int>(a);
    std::string* p_b = getObjPtr<std::string>(b);
    

This works but it is obvious that getObjPtr<int>(b) does not work as intended.

So, my question is:

Is there a way to fix the above code so that we can simply use int* p_a = getObjPtr(a) and std::string* p_b = getObjPtr(b) or better yet auto p_a = getObjPtr(a) and auto p_b = getObjPtr(b)? In other words, is there a way in C++ to instantiate two templates at the same time (if so, we can instantiate the ObjWrap constructor and T* getObjPtr(ObjWrap) at compile time of a ObjWrap object, e.g., ObjWrap a(1))?

Edit 1:

Making ObjWrap a templated class does not help since it defeats the purpose of type erasure.

template <typename T>
class ObjWrap {
  /* ... */
};

ObjWrap<int> a(1); // this is no good for type erasure. 

Edit 2:

I was reading the code and realize that it can be modified to reflect the idea a little better. So, please also look at the following code:

class ObjWrap {
public:
  template <typename T>
  ObjWrap(T O) : Self(new Obj<T>(std::move(O))) {}

  template <typename T>
  T * getObjPtr() {
    return static_cast<T*>(Self->getObjPtr_());
  }
private:
  struct Concept {
    virtual ~Concept() = 0;
    virtual void* getObjPtr_() = 0;
  };
  template <typename T>
  struct Obj : Concept {
    Obj(T O) : Data(std::move(O)) {}
    void* getObjPtr_() { return static_cast<void*>(&Data); }

    T Data;
  };

  std::unique_ptr<Concept> Self;
};

int main() {
  ObjWrap a(1);
  ObjWrap b(std::string("b"));
  int* p_a = a.getObjPtr<int>();
  std::string* p_b = b.getObjPtr<std::string>();

  std::cout << *p_a << " " << *p_b << "\n";

  return 0;
}

The main difference between this version of the code versus the one above is that T * getObjPtr() is a member function that is encapsulated by the ObjWrap object.

Edit 3:

My question regarding type erasure is answered by accepted answer. However, the question on simultaneous type instantiation to multiple templates is yet to be answered. My guess is currently C++ does not allow it but it would be nice to hear from people with more experience on that.


Solution

  • There are a few things that may help.

    First thing to say is that if Obj ever needs to expose the address of the object, it's not Sean Parent's 'inheritance is the root of all evil' type-erasing container.

    The trick is to ensure that the interface of Obj offers all semantic actions and queries the wrapper will ever need.

    In order to provide this, it's often a reasonable idea to cache the address of the object and its type_id in the concept.

    Consider the following updated example, in which there is one public method - operator==. The rule is that two Objs are equal if they contain the same type of object and those objects compare equal.

    Note that the address and type_id:

    1) are implementation details and not exposed on the interface of Obj

    2) are accessible without virtual calls, which short-circuits the not-equal case.

    #include <memory>
    #include <utility>
    #include <typeinfo>
    #include <utility>
    #include <cassert>
    #include <iostream>
    
    class ObjWrap 
    {
    public:
        template <typename T>
        ObjWrap(T O) : Self(new Model<T>(std::move(O))) {}
    
        // objects are equal if they contain the same type of model
        // and the models compare equal
        bool operator==(ObjWrap const& other) const
        {
            // note the short-circuit when the types are not the same
            // this means is_equal can guarantee that the address can be cast
            // without a further check
            return Self->info == other.Self->info
            && Self->is_equal(other.Self->addr);
        }
    
        bool operator!=(ObjWrap const& other) const
        {
            return !(*this == other);
        }
    
        friend std::ostream& operator<<(std::ostream& os, ObjWrap const& o)
        {
            return o.Self->emit(os);
        }
    
    private:
        struct Concept 
        {
            // cache the address and type here in the concept.
            void* addr;
            std::type_info const& info;
    
            Concept(void* address, std::type_info const& info)
            : addr(address)
            , info(info)
            {}
    
            virtual ~Concept() = default;
    
            // this is the concept's interface    
            virtual bool is_equal(void const* other_address) const = 0;
            virtual std::ostream& emit(std::ostream& os) const = 0;
        };
    
        template <typename T>
        struct Model : Concept 
        {
            Model(T O) 
            : Concept(std::addressof(Data), typeid(T))
            , Data(std::move(O)) {}
    
            // no need to check the pointer before casting it.
            // Obj takes care of that
            /// @pre other_address is a valid pointer to a T    
            bool is_equal(void const* other_address) const override
            {
                return Data == *(static_cast<T const*>(other_address));
            }
    
            std::ostream& emit(std::ostream& os) const override
            {
                return os << Data;
            }
    
            T Data;
        };
    
    
    std::unique_ptr<Concept> Self;
    };
    
    
    int main()
    {
        auto x = ObjWrap(std::string("foo"));
        auto y = ObjWrap(std::string("foo"));
        auto z = ObjWrap(int(2));
    
        assert(x == y);
        assert(y != z);
    
        std::cout << x << " " << y << " " << z << std::endl;
    }
    

    http://coliru.stacked-crooked.com/a/dcece2a824a42948