3

Problem:

Multiple components wish to share the same immutable data (of arbitrary type) but each component has a different set of interface requirements.

For example, component A may require that there is a free function available called hash_code(const object&) and component B may require that the object has a free function available called type(const object&).

If component C makes a shared pointer to some object O, for which there is a free function available called hash_code and a free function called type, then component C can share O with components A and B, because a compatible interface can be deduced.

Now we have decoupled polymorphic identity types with no copy overhead.

The way I achieve this is to have a template handle class which is imbued with method descriptors (more template classes):

Here is a full, working test program with two approaches:

One is to manually write the handle class, the other is to allow template expansion to write it for me.

At the moment, there are two problems:

  1. In the template version I have had to resort to virtual inheritance in order to create the model/concept hierarchy.
  2. Describing each method call requires a complex class containing 3 related types: the polymorphic concept, the typed model of that concept plus the handle's interface component.

There has to be a more elegant way...

#include <iostream>
#include <utility>
#include <string>
#include <typeindex>
#include <boost/functional/hash.hpp>


// some arbitrary type which supports hashing and type
struct algo1_ident
{
    std::type_info const& _algo_type = typeid(algo1_ident);
    std::string arg1;
    std::string arg2;
};

auto type(algo1_ident const& ident) -> std::type_info const&
{
    return ident._algo_type;
}

auto hash_code(algo1_ident const& ident) -> std::size_t
{
    auto seed = ident._algo_type.hash_code();
    boost::hash_combine(seed, ident.arg1);
    boost::hash_combine(seed, ident.arg2);
    return seed;
}


//
// manual approach
//
// step one: define a complete concept
struct algo_ident_concept
{
    virtual std::type_info const & impl_type(const void* p) const = 0;
    virtual std::size_t impl_hash_code(const void* p) const = 0;

};

//
// step two: define the model of that concept
//

template<class Impl>
struct algo_ident_model : algo_ident_concept
{
    std::type_info const& impl_type(const void* p) const override
    {
        return type(ref(p));
    }

    std::size_t impl_hash_code(const void* p) const override
    {
        return hash_code(ref(p));
    }

private:
    static const Impl& ref(const void* p) {
        return *static_cast<Impl const*>(p);
    }
};

//
// step three: write the handle class
//
template<template <class> class Model, template <class> class PtrType>
struct algo_ident_handle
{
    template<class Impl> struct model_tag {};

    template<class Impl>
    algo_ident_handle(std::shared_ptr<const Impl> ptr)
    : _impl(std::move(ptr))
    , _access_model(make_access_model(model_tag<const Impl>()))
    {}

    template<class Impl>
    algo_ident_concept const* make_access_model(model_tag<Impl>)
    {
        static struct : algo_ident_concept
        {
            std::type_info const& impl_type(const void* p) const override
            {
                using ::type;
                return type(ref(p));
            }

            std::size_t impl_hash_code(const void* p) const override
            {
                using ::hash_code;
                return hash_code(ref(p));
            }

        private:
            static const Impl& ref(const void* p) {
                return *static_cast<Impl const*>(p);
            }
        } const _model {};
        return std::addressof(_model);
    }

    PtrType<const void> _impl;
    const algo_ident_concept* _access_model;

    //
    // interface
    //

    std::size_t hash_code() const {
        return _access_model->impl_hash_code(_impl.get());
    }

    std::type_info const& type() const {
        return _access_model->impl_type(_impl.get());
    }

};


//
// now the componentised approach
//

// step 1: define the concept, model and handle interface for supporting the
//         method `hash_code`
//         This can go in a library

template<class Host>
struct has_hash_code
{
    struct concept
    {
        virtual std::size_t hash_code(const void*) const = 0;
    };

    template<class Impl> struct model : virtual concept
    {
        std::size_t hash_code(const void* p) const override
        {
            using ::hash_code;
            return hash_code(*static_cast<const Impl*>(p));
        }
    };

    struct interface
    {
        std::size_t hash_code() const
        {
            auto self = static_cast<const Host*>(this);
            return self->model()->hash_code(self->object());
        }
    };
};

// step 2: define the concept, model and handle interface for supporting the
//         method `type`
//         This can go in a library

template<class Host>
struct has_type
{
    struct concept
    {
        virtual std::type_info const& type(const void*) const = 0;
    };

    template<class Impl> struct model : virtual concept
    {
        std::type_info const& type(const void* p) const override
        {
            using ::type;
            return type(*static_cast<const Impl*>(p));
        }
    };

    struct interface
    {
        std::type_info const& type() const
        {
            auto self = static_cast<const Host*>(this);
            return self->model()->type(self->object());
        }
    };
};

// step 3: provide a means of turning a pack of methods into a concept base class

template<class Host, template<class>class...Methods>
struct make_concept
{
    using type = struct : virtual Methods<Host>::concept... {};
};

// step 4: provide a means of turning a pack of methods into a model class

template<class Impl, class Host, template<class>class...Methods>
struct make_model
{
    using concept_type = typename make_concept<Host, Methods...>::type;
    using type = struct : Methods<Host>::template model<Impl>... , concept_type {};

    static auto apply()
    {
        static const type _model {};
        return std::addressof(_model);
    }
};

// step 5: provide a means of turning a pack of methods into an interface

template<class Host, template<class>class...Methods>
struct make_interface
{
    using type = struct : Methods<Host>::interface... {};
};

// step 6: convenience class in which to store the object pointer and the
//         polymorphic model

template<class ConceptType>
struct storage
{
    storage(std::shared_ptr<const void> object, const ConceptType* concept)
    : _object(object), _model(concept)
    {}

    const void* object() const { return _object.get(); }
    const ConceptType* model() const { return _model; }

    std::shared_ptr<void const> _object;
    const ConceptType* _model;
};

// step 7: build a handle which supports the required methods while
//         storing a shared_ptr to the object

template<template<class> class...Methods>
struct handle
: make_interface<handle<Methods...>, Methods...>::type
{
    using this_class = handle;
    using concept_type = typename make_concept<this_class, Methods...>::type;
    using storage_type = storage<concept_type>;

    template<class Impl>
    static auto create_storage(std::shared_ptr<Impl> ptr)
    {
        using model_type = typename make_model<Impl, this_class, Methods...>::type;
        const model_type* pm = make_model<Impl, this_class, Methods...>::apply();
        return storage_type(ptr, pm);
    }

    template<class Impl>
    handle(std::shared_ptr<Impl> ptr)
    : _storage(create_storage(ptr))
    {}

    const void* object() const { return _storage.object(); }
    const concept_type* model() const { return _storage.model(); }


    storage<concept_type> _storage;
};


//
// another arbitrary object which also supports the hash_code and type protocols

namespace algo2 {
    struct algo2_ident
    {
        std::type_info const& _algo_type = typeid(algo2_ident);
        std::string arg1 = "foo";
        std::string arg2 = "bar";
    };

    auto type(algo2_ident const& ident) -> std::type_info const&
    {
        return ident._algo_type;
    }

    auto hash_code(algo2_ident const& ident) -> std::size_t
    {
        auto seed = ident._algo_type.hash_code();
        boost::hash_combine(seed, ident.arg1);
        boost::hash_combine(seed, ident.arg2);
        return seed;
    }
}

//
// test
//

int main(int argc, const char * argv[])
{
    algo_ident_handle<algo_ident_model, std::shared_ptr> h1 = std::make_shared<const algo1_ident>();
    algo_ident_handle<algo_ident_model, std::shared_ptr> h2 = std::make_shared<const algo2::algo2_ident>();

    //
    // prove that an h1 is equivalent to the object of which it is a handle
    //
    algo1_ident chk1 {};
    std::cout << h1.hash_code() << std::endl;
    std::cout << hash_code(chk1) << std::endl;

    algo2::algo2_ident chk {};

    std::cout << h2.hash_code() << std::endl;
    std::cout << hash_code(chk) << std::endl;

    //
    // same proof for the composed handle
    //
    handle<has_hash_code, has_type> ht1 = std::make_shared<const algo2::algo2_ident>();
    std::cout << ht1.hash_code() << std::endl;
    std::cout << hash_code(chk) << std::endl;

    return 0;
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • http://stackoverflow.com/q/38835747/1774667 makes a value, but adapting to a pointer is easy. Here we make our concept pseudo-methods and a smart any that maintains hooks to use them. All checking is static: adding dynamic checking is possible with a bit of work. – Yakk - Adam Nevraumont Aug 16 '16 at 11:43
  • @Yakk awesome. Thanks. – Richard Hodges Aug 16 '16 at 11:47

1 Answers1

0

I may miss something but following seems to do the job:

// To avoid conflict with name and ADL.
namespace detail
{
    template <typename T>
    decltype(auto) callHashCode(T&& t) { return hash_code(std::forward<T>(t)); }

    template <typename T>
    decltype(auto) callType(T&& t) { return type(std::forward<T>(t)); }
}

class HashRunner
{
public:
    template <typename T>
    HashRunner(std::shared_ptr<T> p) :
    hash_code([=](){ return detail::callHashCode(*p); })
    {}

    std::function<std::size_t()> hash_code;
};

class TypeRunner
{
public:
    template <typename T>
    TypeRunner(std::shared_ptr<T> p) :
    type([=]() -> const std::type_info& { return detail::callType(*p); })
    {}

    std::function<const std::type_info&()> type;
};

template <typename ... Ts>
class MyHandle : public Ts...
{
public:
    template <typename T>
    MyHandle(std::shared_ptr<T> p) : Ts(p)... {}
};

Demo.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • It does do the job, but: 1 it's making multiple copies of the shared pointer, 2 the handle class gets bigger as the number of methods increases, which makes it less lightweight to copy. Nice use of std::function though to provide the necessary polymorphism. – Richard Hodges Aug 16 '16 at 06:48