在 c++ 中,通常比较继承层次结构中的对象

Generically comparing objects in an inheritance hierarchy in c++

本文关键字:继承 层次结构 对象 比较 c++ 常比较      更新时间:2023-10-16

我打算写一个这样的多映射

std::multimap <key, base_ptr> mymap;

我希望能够存储许多派生类(例如Der1Der2)的指针,这些派生类从基派生。

现在,当我尝试将对象插入地图时,我首先对键进行查找,然后我需要比较该对象是否等效(不必是同一对象,因此不进行指针比较)与该位置的对象。因此,假设我覆盖了== operator或编写了某种比较函数。现在,我想为此编写代码,以便在添加新的派生类时,我不必更改或添加任何内容。

所以我认为必须有一种通用的方法来写这个。但想不出一个。

我在想以下几件事

class Base
{
virtual Base * get() { return this; }
virtual bool isEqual(const Base& toObj) {
....
}
}
class Der1
{
Der1 * get() { return this; }
bool isEqual(const Der1& toObj) {
....
}
}

但这似乎也不起作用。 因为当我这样做时:

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

我看到对get()的调用确实如我预期的那样最终出现在派生类的get()中,但随后编译器将返回的值视为Base*。这很可能是因为它是运行时多态性。但我发现很难相信不会有一种优雅而明显的方式来做到这一点。

有人可以建议。

你可以尝试这样的事情:

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);
}

这很简单,请阅读此代码的注释:

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 calln");
}
};
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 calln");
}
};
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);
}

我更喜欢dynamic_cast的方式,但是,您所做的功能毫无用处,但我更喜欢使用这些函数,有时如下所示:

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

注意在投射之前,你不需要比较,至少我会这样做。

等价关系不合适。std::multimap和所有有序的关联容器都需要排序关系。因此,如果这是一个严格的要求,则应使用无序容器,即unordered_multimap。

无论哪种情况,您都需要提供一个函数对象,该对象接受两个Base*参数a,b并返回一个bool,该根据您的定义表示是a<b还是a==b

遗憾的是,C++一次只允许对一种类型的虚拟方法进行运行时查找,这里有两种。克服此限制的一种方法是双重调度方法。这样,通用的双参数函数对象

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

最终将调用两个对象之一的正确isEqual,例如,如果a是这种类型的Der1定义。现在在Der1,一般定义是

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

不幸的是,此时您必须在每个派生类中定义几个重载方法,例如isEqual(const Der1&)isEqual(const Der&)

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) { ... }
};

请注意,只有上面的前isEqual是虚拟的,并覆盖Base的方法。其余的是非虚拟重载,调用x.isEqual(*this)将找到合适的重载,因为当*this类型为Der2&时,isEqual(const Der2& x)将优先于isEqual(const Base& x)(当然还有剩余的重载)。

这将顺利工作,而无需任何dynamic_cast或恒定的运行时ifswitch语句。但是,对于n派生类,您需要在最坏的情况下n * (n+1)isEqual的定义(除非您利用层次结构中的常见模式并节省成本)。

此外,此方法违背了"添加新的派生类时,我不必更改或添加任何内容"的要求。再说一次,我不知道您如何期望不更改任何内容 - 您将如何比较新的派生类型?

很抱歉,我不知道任何真正优雅的解决方案。一般来说,如果可能的话,我更喜欢静态多态性,但在这里你需要一个单一类型的项目的容器,所以这不适用。

您可以使用多个调度:

以下内容可能会有所帮助(需要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