Search code examples
c++c++17sfinaetype-erasure

Template constructor resolution based on existence of method or free-function


Problem

Motivated by Sean Parent's "Runtime Polymorphism" I implemented a Serializable class that uses type-erasure to dispatch Serializable::serialize(...)obj.serialize(...), where obj is a wrapped object.

struct Serializable
{
    template <typename T>
    Serializable(T obj)
        : m_self(std::make_unique<Model<T> >(std::move(obj))) {}

    /// Writes itself to a write storage
    void serialize(Storage& outStream)
    { return m_self->serialize(outStream); }  
private:
    struct Concept
    {
        virtual ~Concept() = default;
        virtual void serialize(Storage& outStream) = 0;
    };

    template <typename T>
    class Model final : public Concept
    {
    public:
        Model(T x) : m_data(std::move(x)) {}
    private:
        void serialize(Storage& outStream) override
        { m_data.serialize(outStream); } 
    private:
        T m_data;
    }; 
private:
    std::unique_ptr<Concept> m_self;
};

Now I would like to extend Serializable with another model class that would dispatch Serializable::serialize(...) to a free function with obj as an argument: Serializable::serialize(...)serialize(obj, ...)

Then I would like a template constructor of Serializable to decide which model to use by checking the existence of either T::serialize(...) or serialize(const T&, ...)

Question

Is it possible by any means (e.g., SFINAE) to automatically construct Serializable so that it uses a method serialization if possible and free-function serialization otherwise?

Feel free to use any C++ standard up to C++17.


Solution

  • You can devise your own trait to find out whether the class has the correct serialize member. There are several ways to do it, this is one of them:

    template <class T, class = void>
    struct HasMemberSerialize : std::false_type
    {};
    
    template <class T>
    struct HasMemberSerialize<T, std::void_t<decltype(std::declval<T>().serialize(std::declval<Storage&>()))>> : std::true_type
    {};
    

    [Live example]

    Then, add a new template parameter to Model and use the trait to find its argument:

    struct Serializable
    {
        template <typename T>
        Serializable(T obj)
            : m_self(std::make_unique<Model<T, HasMemberSerialize<T>::value> >(std::move(obj))) {}
    
        /// Writes itself to a write storage
        void serialize(Storage& outStream)
        { return m_self->serialize(outStream); }  
    private:
        struct Concept
        {
            virtual ~Concept() = default;
            virtual void serialize(Storage& outStream) = 0;
        };
    
        template <typename T, bool Member>
        class Model;
    private:
        std::unique_ptr<Concept> m_self;
    };
    
    template <typename T>
    class Serializable::Model<T, true> final : public Serializable::Concept
    {
    public:
        Model(T x) : m_data(std::move(x)) {}
    private:
        void serialize(Storage& outStream) override
        { m_data.serialize(outStream); } 
    private:
        T m_data;
    };
    
    template <typename T>
    class Serializable::Model<T, false> final : public Serializable::Concept
    {
    public:
        Model(T x) : m_data(std::move(x)) {}
    private:
        void serialize(Storage& outStream) override
        { serialize(m_data, outStream); } 
    private:
        T m_data;
    };