Generically comparing objects in an inheritance hierarchy in c++

寵の児 提交于 2019-12-11 13:48:03

问题


I am planning to write a multimap like this

std::multimap <key, base_ptr> mymap;

And I would like to be able to store pointers of many derived classes (say Der1, Der2) which derive from the base.

Now when I am trying to insert an object into the map, I first do a lookup on the key and then I need to compare if the object is EQUIVALENT (does not have to be the same object hence not doing a pointer comparison) to the one at that location. So for this lets say I override the == operator or write some kind of a compare function. Now I would like to write the code for this in such a manner that when new derived classes are added, I dont have to change or add anything.

So I am thinking there has to be a generic way of writing this. But not able to think of one.

I was thinking of something like the following

class Base
{
    virtual Base * get() { return this; }

    virtual bool isEqual(const Base& toObj) {
        ....
    }
}

class Der1
{
    Der1 * get() { return this; }

    bool isEqual(const Der1& toObj) {
        ....
    }
}

But this does not seem to work either. because when I do:

Base* bp1;
Base* bp2;
bp1->get()->isEqual(*(bp2->get()))

I see that the call to get() does end up in the get() of the derived class as I expect, but then the compiler treats the returned value as Base*. This is most likely because its a run-time polymorphism. But I find it hard to believe that there wont be an elegant and obvious way to do this.

Can somebody advise.


回答1:


You might try something like this:

class Base
{
    public:

    // derived classes must implement this.
    // the implementation is always the same code
    virtual type_info getType() const = 0;

    virtual bool isEqual(const Base& toObj) = 0;
}

class Der1 : Base
{
    public:
    type_info getType() const { return typeid (this); }

    bool isEqual(const Base& toObj)
    {
        if (this.getType() == toObj.getType())
        {
            // we have 2 instances of Der1!
            // do comparison here
        }
        else
        {
            // the other one is no Der1 => we are not equal
            return false;
        }
    }
}

class Der2 : Base
{
    public:
    type_info getType() const { return typeid (this); }

    bool isEqual(const Base& toObj)
    {
        if (this.getType() == toObj.getType())
        {
            // we have 2 instances of Der2!
            // do comparison here
        }
        else
        {
            // the other one is no Der1 => we are not equal
            return false;
        }
    }
}



void MyFunc()
{
    Base* bp1;
    Base* bp2;
    // ...
    // this should work now (although I have not tested it!)
    bool theyAreEqual = bp1->isEqual(*bp2);
}



回答2:


It's quite simple, read the comments on this code:

class Dervived;
class Base
{
public:
    /* Note:
     * You can get rid of those functions and use dynamic_cast instead
     * which might be better.  See down for example.  */
    const Base *getBase() const { return this; }
    virtual const Dervived *getDervived() const { return NULL; }

    virtual bool operator==(const Base& other) const
    {
        printf("base call\n");
    }
};

class Dervived : public Base
{
public:
    const Dervived *getDervived() const { return this; }

    bool operator==(const Base& other) const
    {
            // case Base to Dervived here, either with dynamic_cast or C-style cast, but since you're very sure that other is Dervived, you can go for static_cast right away and no need to check for cast success.
        printf("dervive call\n");
    }
};

int main()
{
    Base *b = new Dervived();
    Base *b2 = new Dervived();

    if (*b == *b2);     // calls Dervived::operator==
        // ...

    /* An alternative way of casting:  */
    const Dervived *d = dynamic_cast<Dervived *>(b);
    if (d);
        // cast successfull
    /* Or use the members.  */
    d = b->getDervived();
    if (d);
}

I prefer the dynamic_cast way, however, the functions you made are quite useless, but i prefer to use these kind of functions sometimes like so:

class Base
{
...
virtual bool isDervived() const { return false; }
};

class Dervived
{
...
bool isDervived() const { return true; }
};

Note You shouldn't need to compare before casting, at least this is what I would do.




回答3:


An equivalence relation is not appropriate. std::multimap and all ordered associative containers need an ordering relation. So if this a strict requirement, you should use an unordered container, that is, unordered_multimap.

In either case, you need to provide a function object that takes two Base* arguments a,b and returns a bool that says whether a<b or a==b according to your definition.

Unfortunately, C++ permits run-time lookup for virtual methods only for one type at a time, and here you have two. One way over this limitation is the double dispatch method. This way, the generic two-argument function object

struct eq
{
    bool operator()(const Base& a, const Base& b)
    {
        return a.isEqual(b);
    }
};

will end up calling the right isEqual of one of the two objects, e.g. the definition of Der1 if a is of this type. Now in Der1, the general definition would be

bool isEqual(const Base& x) { return x.isEqual(*this); }

Unfortunately at this point you would have to define several overloaded methods in each derived class e.g. isEqual(const Der1&), isEqual(const Der&) etc.

class Der1
{
    // ...

    bool isEqual(const Base& x) { return x.isEqual(*this); }
    bool isEqual(const Der1& x) { ... }
    bool isEqual(const Der2& x) { ... }
    bool isEqual(const Der3& x) { ... }
};

Note that only the first isEqual above is virtual and overrides Base's method. The rest are non-virtual overloads and the call x.isEqual(*this) will find the appropriate one because when *this is of type Der2& then isEqual(const Der2& x) will be preferred over isEqual(const Base& x) (and the remaining overloads of course).

This will work smoothly without the need for any dynamic_cast or constly run-time if or switch statements. However, for n derived classes you need n * (n+1) definitions of isEqual in the worst case (except if you exploit common patterns in the hierarchy and make savings).

Also, this approach defeats your requirement that "when new derived classes are added, I dont have to change or add anything". Then again, I don't know how you'd expect not to change anything - how would you compare a new derived type?

In am sorry I don't know any really more elegant solution. In general I prefer static polymorphism when possible but here you need a container of items of a single type so this does not apply.




回答4:


You may use Multiple dispatch:

Following may help (require C++11): http://ideone.com/lTsc7M

#include <cstdint>
#include <array>
#include <iostream>
#include <tuple>
#include <type_traits>

/////////////////////////

#if 1 // multiple dispatch

// sequence of size_t // not in C++11
template <std::size_t ...> struct index_sequence {};

// Create index_sequence<0, >
template <std::size_t N, std::size_t ...Is>
struct make_index_sequence : make_index_sequence <N - 1, N - 1, Is... > {};

template <std::size_t ... Is>
struct make_index_sequence<0, Is...> : index_sequence<Is...> {};

// Generic IVisitor
// Do: using MyIVisitor = IVisitorTs<Child1, Child2, ...>
template <typename ... Ts> class IVisitorTs;

template <typename T, typename ... Ts>
class IVisitorTs<T, Ts...> : public IVisitorTs<Ts...>
{
public:
    using tuple_type = std::tuple<T, Ts...>;
    using IVisitorTs<Ts...>::visit;

    virtual void visit(const T& t) = 0;
};

template <typename T> class IVisitorTs<T>
{
public:
    using tuple_type = std::tuple<T>;

    virtual void visit(const T& t) = 0;
};

namespace detail {

// retrieve the index of T in Ts...
template <typename T, typename ... Ts> struct get_index;

template <typename T, typename ... Ts>
struct get_index<T, T, Ts...> : std::integral_constant<std::size_t, 0> {};

template <typename T, typename Tail,  typename ... Ts>
struct get_index<T, Tail, Ts...> :
        std::integral_constant < std::size_t, 1 + get_index<T, Ts...>::value > {};

// retrieve the index of T in Tuple<Ts...>
template <typename T, typename Tuple> struct get_index_in_tuple;

template <typename T, template <typename...> class C, typename ... Ts>
struct get_index_in_tuple<T, C<Ts...>> : get_index<T, Ts...> {};

// get element of a multiarray
template <std::size_t I>
struct multi_array_getter
{
    template <typename T, std::size_t N>
    static constexpr auto get(const T& a, const std::array<std::size_t, N>& index)
    -> decltype(multi_array_getter<I - 1>::get(a[index[N - I]], index))
    {
        return multi_array_getter<I - 1>::get(a[index[N - I]], index);
    }
};

template <>
struct multi_array_getter<0>
{
    template <typename T, std::size_t N>
    static constexpr auto get(const T& a, const std::array<std::size_t, N>& index)
    -> decltype(a)
    {
        return a;
    }
};

// Provide an implementation of visitor
// by forwarding to C implementation (which may be non virtual)
template <typename IVisitor, typename C, typename...Ts> struct IVisitorImpl;

template <typename IVisitor, typename C, typename T, typename...Ts>
struct IVisitorImpl<IVisitor, C, T, Ts...> : IVisitorImpl<IVisitor, C, Ts...>
{
    virtual void visit(const T& t) override { C::visit(t); }
};

template <typename IVisitor, typename C, typename T>
struct IVisitorImpl<IVisitor, C, T> : IVisitor, C
{
    virtual void visit(const T& t) override { C::visit(t); }
};

// helper to expand child type to IVisitorImpl
template <typename IVisitor, typename C>
struct IVisitorImplType;

template <typename ... Ts, typename C>
struct IVisitorImplType<IVisitorTs<Ts...>, C>
{
    using type = IVisitorImpl<IVisitorTs<Ts...>, C, Ts...>;
};

// Create an multi array of pointer of function
// (with all combinaisons of overload).
template <typename Ret, typename F, typename Arg>
class GetAllOverload
{
private:
    template <typename...Ts>
    struct Functor
    {
        // function which will be in array.
        static Ret call(F&f, const Arg& arg)
        {
            return call_helper(f, arg, make_index_sequence<sizeof...(Ts)>());
        }
    private:
        // The final dispatched function
        template <std::size_t ... Is>
        static Ret call_helper(F&f, const Arg& arg, index_sequence<Is...>)
        {
            using RetTuple = std::tuple<Ts&...>;
            // static cast is suffisant if arg is the abstract type
            // when given arg is concrete type, reinterpret_cast is required.
            // TODO: build a smaller table with only possible value to avoid that
            return f(reinterpret_cast<typename std::tuple_element<Is, RetTuple>::type>(std::get<Is>(arg))...);
        }
    };

    // helper class to create the multi array of function pointer
    template <std::size_t N, typename Tuple, typename...Ts>
    struct Builder;

    template <typename...Ts, typename...Ts2>
    struct Builder<1, std::tuple<Ts...>, Ts2...>
    {
        using RetType = std::array<Ret (*)(F&, const Arg&), sizeof...(Ts)>;

        static constexpr RetType build()
        {
            return RetType{ &Functor<Ts2..., Ts>::call... };
        }
    };

    template <std::size_t N, typename ...Ts, typename...Ts2>
    struct Builder<N, std::tuple<Ts...>, Ts2...>
    {
        template <typename T>
        using RecType = Builder<N - 1, std::tuple<Ts...>, Ts2..., T>;
        using T0 = typename std::tuple_element<0, std::tuple<Ts...>>::type;
        using RetType = std::array<decltype(RecType<T0>::build()), sizeof...(Ts)>;

        static constexpr RetType build() {
            return RetType{ RecType<Ts>::build()... };
        }
    };

public:
    template <std::size_t N, typename VisitorTuple>
    static constexpr auto get()
    -> decltype(Builder<N, VisitorTuple>::build())
    {
        return Builder<N, VisitorTuple>::build();
    }
};

template <typename Ret, typename IVisitor, typename F, std::size_t N>
class dispatcher
{
private:
    std::array<std::size_t, N> index;

    struct visitorCallImpl
    {
        template <typename T>
        void visit(const T&) const
        {
            *index = get_index_in_tuple<T, IVisitor>::value;
        }

        void setIndexPtr(std::size_t& index) { this->index = &index; }
    private:
        std::size_t* index = nullptr;
    };

    template <std::size_t I, typename Tuple>
    void set_index(const Tuple&t)
    {
        using VisitorType = typename IVisitorImplType<IVisitor, visitorCallImpl>::type;
        VisitorType visitor;
        visitor.setIndexPtr(index[I]);

        std::get<I>(t).accept(visitor);
    }
public:
    template <typename Tuple, std::size_t ... Is>
    Ret operator () (F&& f, const Tuple&t, index_sequence<Is...>)
    {
        const int dummy[] = {(set_index<Is>(t), 0)...};
        static_cast<void>(dummy); // silent the warning unused varaible
        constexpr auto a = GetAllOverload<Ret, F&&, Tuple>::
            template get<sizeof...(Is), typename IVisitor::tuple_type>();
        auto func = multi_array_getter<N>::get(a, index);
        return (*func)(f, t);
    }
};

} // namespace detail

template <typename Ret, typename Visitor, typename F, typename ... Ts>
Ret dispatch(F&& f, Ts&...args)
{
    constexpr std::size_t size = sizeof...(Ts);
    detail::dispatcher<Ret, Visitor, F&&, size> d;
    return d(std::forward<F>(f), std::tie(args...), make_index_sequence<size>());
}

#endif // multiple dispatch

#if 1 // multiple dispatch usage
struct Square;
struct Rect;
struct Circle;

using IShapeVisitor = IVisitorTs<Square, Rect, Circle>;

struct IShape {
    virtual ~IShape() = default;
    virtual void accept(IShapeVisitor&) const = 0;
};
struct Rect : IShape {
    virtual void accept(IShapeVisitor& v) const override { v.visit(*this); }
};

struct Square : Rect {
    virtual void accept(IShapeVisitor& v) const override { v.visit(*this); }
};
struct Circle : IShape {
    virtual void accept(IShapeVisitor& v) const override { v.visit(*this); }
};

class ShapePrinter : public IShapeVisitor
{
public:
    void visit(const Rect& s) override { std::cout << "Rect"; }
    void visit(const Square& s) override { std::cout << "Square"; }
    void visit(const Circle& s) override { std::cout << "Circle"; }
};

struct IsEqual
{
    bool operator() (IShape& s1, IShape& s2) const
    {
        ShapePrinter printer;
        s1.accept(printer);
        std::cout << " != ";
        s2.accept(printer);
        std::cout << std::endl;
        return false;
    }

    template <typename S>
    bool operator() (S& s1, S& s2) const
    {
        ShapePrinter printer;
        s1.accept(printer);
        std::cout << " == ";
        s2.accept(printer);
        std::cout << std::endl;
        return true;
    }
};

int main(int argc, char *argv[])
{
    Rect rect;
    Square sq;
    Circle c;
    IShape* shapes[] = { &rect, &sq, &c };

    for (auto shape1 : shapes) {
        for (auto shape2 : shapes) {
            dispatch<bool, IShapeVisitor>(IsEqual(), *shape1, *shape2);
        }
    }
    return 0;
}

#endif // multiple dispatch usage


来源:https://stackoverflow.com/questions/21529062/generically-comparing-objects-in-an-inheritance-hierarchy-in-c

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!