Сравнение элегантных объектов - PullRequest
9 голосов
/ 16 ноября 2008

При сравнении двух объектов (одного типа) имеет смысл иметь функцию сравнения, которая принимает другой экземпляр того же класса. Если я реализую это как виртуальную функцию в базовом классе, то сигнатура функции должна ссылаться и на базовый класс в производных классах. Каков элегантный способ справиться с этим? Разве Сравнение не должно быть виртуальным?

class A
{
    A();
    ~A();
    virtual int Compare(A Other);
}

class B: A
{
    B();
    ~B();
    int Compare(A Other);
}

class C: A
{
    C();
    ~C();
    int Compare(A Other);
}

Ответы [ 8 ]

1 голос
/ 08 мая 2009

Это зависит от предполагаемой семантики A, B и C и семантики сравнения (). Сравнение - это абстрактное понятие, которое не обязательно имеет единственное правильное значение (или вообще какое-либо значение). На этот вопрос нет однозначного правильного ответа.

Вот два сценария, где сравнение означает две совершенно разные вещи с одинаковой иерархией классов:

class Object 
{
    virtual int compare(const Object& ) = 0;
    float volume;
};

class Animal : Object 
{
    virtual int compare(const Object& );
    float age;
};

class Zebra  : Animal 
{
    int compare(const Object& );
};

Мы можем рассмотреть (как минимум) два способа сравнения двух зебр: какая старше и какая больше? Оба сравнения корректны и легко вычислимы; Разница в том, что мы можем использовать объем, чтобы сравнить Зебру с любым другим Объектом, но мы можем использовать только возраст, чтобы сравнить Зебр с другими Животными. Если мы хотим, чтобы compare () реализовала семантику сравнения возраста, не имеет смысла определять compare () в классе Object, так как семантика не определена на этом уровне иерархии. Стоит отметить, что ни один из этих сценариев не требует какого-либо приведения, так как семантика определяется на уровне базового класса (будь то Object при сравнении объема или Animal при сравнении возраста).

Это поднимает более важную проблему - некоторые классы не подходят для одной функции catch (all) all compare (). Часто имеет смысл реализовывать несколько функций, которые явно указывают на то, что сравнивается, например, Compare_age () и Compare_volume (). Определение этих функций может происходить в той точке иерархии наследования, где семантика становится актуальной, и адаптировать их к дочерним классам должно быть тривиально (если вообще требуется адаптация). Простое сравнение с использованием Compare () или оператора == () часто имеет смысл только с простыми классами, где правильная семантическая реализация очевидна и однозначна.

Короче говоря ... "это зависит".

1 голос
/ 16 ноября 2008

Наверное, я бы сделал это так:

class A
{
 public:
  virtual int Compare (const A& rhs) const
  {
    // do some comparisons
  }
};

class B
{
 public:
  virtual int Compare (const A& rhs) const
  {
    try
    {
      B& b = dynamic_cast<A&>(rhs)
      if (A::Compare(b) == /* equal */)
      {
        // do some comparisons
      }
      else
        return /* not equal */;
    }
    catch (std::bad_cast&)
    {
      return /* non-equal */
    }
  }
};
1 голос
/ 16 ноября 2008

Я бы реализовал это так:

class A{
    int a;

public:
    virtual int Compare(A *other);
};


class B : A{
    int b;

public:
    /*override*/ int Compare(A *other);
};

int A::Compare(A *other){
    if(!other)
        return 1; /* let's just say that non-null > null */

    if(a > other->a)
        return 1;

    if(a < other->a)
        return -1;

    return 0;
}

int B::Compare(A *other){
    int cmp = A::Compare(other);
    if(cmp)
        return cmp;

    B *b_other = dynamic_cast<B*>(other);
    if(!b_other)
        throw "Must be a B object";

    if(b > b_other->b)
        return 1;

    if(b < b_other->b)
        return -1;

    return 0;
}

Это очень похоже на шаблон IComparable в .NET, который работает очень хорошо.

EDIT:

Одно замечание к вышесказанному заключается в том, что a.Compare(b) (где a - это A, а b - это B) может возвращать равенство и никогда никогда не выдаст исключение, тогда как b.Compare(a) будут. Иногда это то, что вы хотите, а иногда нет. Если это не так, то вы, вероятно, не хотите, чтобы ваша функция Compare была виртуальной, или вы хотите сравнить type_info s в базовой функции Compare, например:

int A::Compare(A *other){
    if(!other)
        return 1; /* let's just say that non-null > null */

    if(typeid(this) != typeid(other))
        throw "Must be the same type";

    if(a > other->a)
        return 1;

    if(a < other->a)
        return -1;

    return 0;
}

Обратите внимание, что функции Compare производных классов не должны изменяться, поскольку они должны вызывать Compare базового класса, где будет происходить сравнение type_info. Однако вы можете заменить dynamic_cast в переопределенной функции Compare на static_cast.

0 голосов
/ 16 ноября 2008

Я бы предложил не делать это виртуальным. Единственным недостатком является то, что вы явно должны сказать, какое сравнение использовать, если классы не совпадают. Но так как вы должны это сделать, вы можете обнаружить ошибку (во время компиляции), которая в противном случае может вызвать ошибку времени выполнения ...

class A
{
  public:
    A(){};
    int Compare(A const & Other) {cout << "A::Compare()" << endl; return 0;};
};

class B: public A
{
  public:
    B(){};
    int Compare(B const & Other) {cout << "B::Compare()" << endl; return 0;};
};

class C: public A
{
  public:
    C(){};
    int Compare(C const & Other) {cout << "C::Compare()" << endl; return 0;};
};

int main(int argc, char* argv[])
{
    A a1;
    B b1, b2;
    C c1;

    a1.Compare(b1);     // A::Compare()
    b1.A::Compare(a1);  // A::Compare()
    b1.Compare(b2);     // B::Compare()
    c1.A::Compare(b1);  // A::Compare()

    return 0;
}
0 голосов
/ 16 ноября 2008

У меня вряд ли есть эта проблема в C ++. В отличие от Java, мы не обязаны наследовать все наши классы от одного и того же корневого класса Object. При работе с сопоставимыми (/ value семантикой) классами маловероятно, чтобы они происходили из полиморфной иерархии.

Если необходимость в вашей конкретной ситуации реальна, вы снова столкнулись с проблемой двойной отправки / мультиметода. Существуют различные способы ее решения (dynamic_cast, таблицы функций для возможных взаимодействий, посетители, ...)

0 голосов
/ 16 ноября 2008

В дополнение к dynamic_cast, вам также необходимо передать ссылку или указатель, вероятно, const. Функция сравнения также может быть постоянной.

class B: public A
{
    B();
    virtual ~B();
    virtual int Compare(const A &Other) const;
};


int B::Compare(const A &Other) const
{
    const B *other = dynamic_cast <const B*> (&Other);
    if(other) {
        // compare
    }
    else {
        return 0;
    }
}

РЕДАКТИРОВАТЬ: должны компилироваться перед публикацией ...

0 голосов
/ 16 ноября 2008

Сравнение должно быть отражающим, поэтому:

let a = new A
let b = new B (inherits from A)

if (a.equals(b))
 then b.equals(a) must be true!

Таким образом, a.equals(b) должен возвращать false, поскольку B, вероятно, содержит поля, которых нет в A, что означает, что b.equals(a), вероятно, будет false.

Таким образом, в C ++ сравнение должно быть виртуальным, я полагаю, и вы должны использовать проверку типов, чтобы убедиться, что параметр имеет «тот же» тип, что и текущий объект.

0 голосов
/ 16 ноября 2008

Если вы имеете в виду, что Compare () в классе B или C всегда должен передаваться объект класса B или C, независимо от того, что говорит подпись, вы можете работать с указателями на экземпляры вместо экземпляров и пытаться уменьшить указатель в коде метода, использующий что-то вроде

int B::Compare(A *ptr)
{
   other = dynamic_cast <B*> (ptr);
   if(other)
      ...  // Ok, it was a pointer to B
}

(Такая перегрузка была бы необходима только для тех производных классов, которые добавляют к состоянию своего родителя что-то, что влияет на сравнение.)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...