views:

180

answers:

2

Let's say I'm working on a library that works on items of type Item. The main entry point is a class like:

class Worker
{
private:
  SomeContainer _c;
public:
    void add( const Item &i );
    void doSomething();
};

The doSomething() method looks at the added items, compares them, etc. and does something with them. So the Item class needs an operator==.

Now, I want to use this library in different environments and in each environment the implementation of the Item class is different. Therefore, Item needs a virtual comparison function:

class Item
{
protected:
    virtual bool _equals( Item &other ) = 0;
public:
    bool operator==( Item &other ) { return _equals( other ); };
};

And each environment has its own Item implementation. The library only knows about the Item class and the specific item classes are defined and implemented in the platform-specific applications using the library. In environment A it might be:

class AItem: public Item
{
private:
    bool _equals( Item &other );
    std::string _member;
...
};

and in environment B:

class BItem: public Item
{
private:
    bool _equals( Item &other );
    int _member;
...
};

What is now the best way to, for each environment, implement the comparison for use by the library? _equals is specified in the Item class , so its specific item implementations need to cast other to their own type.

In a given environment, different item types will not be used at the same time, so given that assumption, the following would be sort-of safe:

bool AItem::_equals( Item &other )
{
    return this->_member == static_cast<AItem &>(other)._member;
}

But it seems like a nasty solution because it allows a programmer using the library to implement a new environment to break things if he adds items of different types to the worker.

Other solutions I can think of are:

  • Use dynamic_cast.
  • Implement my own sort-of-RTTI by adding a member to the base Item class indicating the environment. This can then be used in the comparison function to see if other is of the same type. Not very elegant.
  • Implement all the types in the library and select the right one based on a #define at compile-time. This means the library itself must be extended for each environment.
  • Use a templatized worker.

But I feel there must be a more elegant solution. Maybe something completely different. What would you do?

+3  A: 
  • Wouldn't it better if Worker was templatized on Item instead of relying on inheritance? In general value semantic (and equality is at the earth of value semantic) doesn't work well with inheritance. In your specific example, I also fear truncation if SomeContainer does copy what it stored in it.

  • If you really need an operation which depend on the dynamic type of two objects, you should do search about "multiple dispatch" (or look in "Modern C++ Design", it has a chapter on this subject). There are several known techniques for this with different trade-off. One of the best known one is often associated with the Visitor Pattern. The simplest variant depend on knowing beforhand all the descendants of Item. (If you look at a description of the visitor pattern, take into account that both hierarchies of that pattern would be unified for your application).

Edit: here is the sketch of an example:

class ItemA;
class ItemB;

class Item
{
public:
    virtual bool equal(Item const&) const = 0;
protected:
    virtual bool doesEqual(ItemA const*) const { return false; }
    virtual bool doesEqual(ItemB const*) const { return false; }
};

class ItemA: public Item
{
public:
    virtual bool equal(Item const& other) const
    {
        return other.doesEqual(this);
    }
protected:
    virtual bool doesEqual(ItemA const* other) {
        return do_the_check;
    }
};

class ItemB: public Item
{
public:
    virtual bool equal(Item const& other) const
    {
        return other.doesEqual(this);
    }
protected:
    virtual bool doesEqual(ItemB const* other) {
        return do_the_check;
    }
};

bool operator==(Item const& lhs, Item const& rhs)
{
    return lhs.equal(rhs);
}
AProgrammer
OK, that's a pretty good trade-off because although the library needs to know about the classes, they can be implemented in the specific applications using the library. Good point on the copying! The Item class will need virtual clone functionality I guess, as well as a virtual destructor.
Marten
A: 

Imho, equality is context-dependent. That means: environment A will regard two objects equal on totally different grounds than environment B.

Hence, I think the environment should provide the equality-function.

Another aspect on equality is that it's usually regarded as a symmetrical operation: if a equals b, b equals a. This implies that it cannot be single-dispatch, i.e. implemented as a class method. As a consequence of that, it needs to be dependent on the interfaces of the two objects only. This also enforces the use of an explicit, two-argument equality function provided by the environment.

xtofl
Exactly right, the implementations of equality testing are very different. So you say there should be an equality function outside any class in each specific environment? If it is given to the Worker object as, say, a function pointer, the Worker still needs to know the function signature. And since it only knows about the Item class, the arguments can only be of type Item. So inside the equality function, the items need to be cast to the right type and the problem is essentially the same, isn't it?
Marten
Indeed, the equality function will probably consist of a bool (*eq)( Item,Item ) interface. Although I can imagine a system where the environment asks for a "typed" Worker object (e.g. using a template member function) that uses a more specific type-safe equality function.
xtofl