Почему нет параметра противоречия для переопределения? - PullRequest
25 голосов
/ 08 июня 2010

C ++ и Java поддерживают ковариацию возвращаемого типа при переопределении методов.

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

Почему это ? Мне кажется, что это не повредит. Я могу найти одну причину для этого в Java - так как он в любом случае имеет механизм «выбрать самую конкретную версию» для перегрузки - но не могу придумать ни одной причины для C ++.

Пример (Java):

class A {
    public void f(String s) {...}
}
class B extends A {
    public void f(Object o) {...} // Why doesn't this override A.f?
}

Ответы [ 6 ]

22 голосов
/ 10 июня 2010

По чистой проблеме противоречий

Добавление противоречия к языку открывает множество потенциальных проблем или нечистых решений и дает очень мало преимуществ, поскольку его можно легко смоделировать без поддержки языка:

struct A {};
struct B : A {};
struct C {
   virtual void f( B& );
};
struct D : C {
   virtual void f( A& );     // this would be contravariance, but not supported
   virtual void f( B& b ) {  // [0] manually dispatch and simulate contravariance
      D::f( static_cast<A&>(b) );
   }
};

С помощью простого дополнительного перехода вы можете вручную решить проблему языка, который не поддерживает противоречивость. В этом примере f( A& ) не обязательно должен быть виртуальным, и вызов полностью квалифицирован для блокировки механизма виртуальной диспетчеризации.

Этот подход показывает одну из первых проблем, которая возникает при добавлении противоречивости к языку, который не имеет полной динамической диспетчеризации:

// assuming that contravariance was supported:
struct P {
   virtual f( B& ); 
};
struct Q : P {
   virtual f( A& );
};
struct R : Q {
   virtual f( ??? & );
};

При действии контравариантности Q::f будет переопределением P::f, и это будет хорошо, как для каждого объекта o, который может быть аргументом P::f, этот же объект равен допустимый аргумент Q::f. Теперь, добавив дополнительный уровень в иерархию, мы сталкиваемся с проблемой проектирования: является ли R::f(B&) допустимым переопределением P::f или оно должно быть R::f(A&)?

Без контравариантности R::f( B& ) явно переопределяет P::f, поскольку подпись идеально подходит. После добавления контравариантности к промежуточному уровню проблема заключается в том, что существуют аргументы, которые действительны на уровне Q, но не на уровнях P или R. Чтобы R соответствовал требованиям Q, единственный вариант - принудительное использование подписи R::f( A& ), чтобы можно было скомпилировать следующий код:

int main() {
   A a; R r;
   Q & q = r;
   q.f(a);
}

В то же время в языке нет ничего, препятствующего следующему коду:

struct R : Q {
   void f( B& );    // override of Q::f, which is an override of P::f
   virtual f( A& ); // I can add this
};

Теперь у нас есть забавный эффект:

int main() {
  R r;
  P & p = r;
  B b;
  r.f( b ); // [1] calls R::f( B& )
  p.f( b ); // [2] calls R::f( A& )
}

В [1] есть прямой вызов метода-члена R. Поскольку r является локальным объектом, а не ссылкой или указателем, динамический механизм диспетчеризации отсутствует, и наилучшее совпадение - R::f( B& ). В то же время в [2] вызов осуществляется через ссылку на базовый класс, и включается механизм виртуальной диспетчеризации.

Поскольку R::f( A& ) является переопределением Q::f( A& ), которое, в свою очередь, является переопределением P::f( B& ), компилятор должен вызвать R::f( A& ). Хотя это может быть точно определено в языке, может быть удивительно обнаружить, что два почти точных вызова [1] и [2] фактически вызывают разные методы, и что в [2] система будет вызывать not лучшее совпадение аргументов.

Конечно, можно утверждать иначе: R::f( B& ) должно быть правильным переопределением, а не R::f( A& ). Проблема в этом случае:

int main() {
   A a; R r;
   Q & q = r;
   q.f( a );  // should this compile? what should it do?
}

Если вы проверяете класс Q, предыдущий код совершенно корректен: Q::f принимает A& в качестве аргумента. У компилятора нет причин жаловаться на этот код. Но проблема в том, что согласно этому последнему предположению R::f принимает B&, а не A& в качестве аргумента! Фактическое переопределение, которое было бы на месте, не могло бы обработать аргумент a, даже если сигнатура метода в месте вызова кажется совершенно правильной. Этот путь приводит нас к определению того, что второй путь намного хуже первого. R::f( B& ) не может быть переопределением Q::f( A& ).

Следуя принципу наименьшего удивления, и разработчику компилятора, и программисту намного проще не иметь противоречивости в аргументах функции. Не потому, что это невозможно, а потому, что в коде могут быть причуды и неожиданности, и учитывая, что существуют простые обходные пути, если функция отсутствует в языке.

при перегрузке против сокрытия

Как в Java, так и в C ++, в первом примере (с A, B, C и D) при удалении ручной отправки [0], C::f и D::f - это разные подписи не переопределяет. В обоих случаях они фактически являются перегрузками одного и того же имени функции, с той небольшой разницей, что из-за правил поиска C ++ перегрузка C::f будет скрыта D::f. Но это только означает, что компилятор не найдет перегрузку hidden по умолчанию, а не то, что она отсутствует:

int main() {
   D d; B b;
   d.f( b );    // D::f( A& )
   d.C::f( b ); // C::f( B& )
}

И с небольшим изменением определения класса его можно заставить работать точно так же, как в Java:

struct D : C {
   using C::f;           // Bring all overloads of `f` in `C` into scope here
   virtual void f( A& );
};
int main() {
   D d; B b;
   d.f( b );  // C::f( B& ) since it is a better match than D::f( A& )
}
14 голосов
/ 08 июня 2010
class A {
    public void f(String s) {...}
    public void f(Integer i) {...}
}

class B extends A {
    public void f(Object o) {...} // Which A.f should this override?
}
5 голосов
/ 08 июня 2010

Для C ++ Страуструп обсуждает причины краткого сокрытия в разделе 3.5.3 Проектирование и развитие C ++ . Он рассуждает (я перефразирую), что другие решения поднимают столько же проблем, и так было со времен C With Classes.

В качестве примера он дает два класса - и производный класс B. Оба имеют виртуальную функцию copy (), которая принимает указатель их соответствующих типов. Если мы скажем:

A a;
B b;
b.copy( & a );

, что в настоящее время является ошибкой, так как copy () B скрывает A. Если бы это не было ошибкой, только функции A в B могли бы быть обновлены функцией copy ().

Еще раз, я перефразировал - если вам интересно, прочитайте книгу, которая превосходна.

3 голосов
/ 08 июня 2010

Хотя это приятно иметь на любом оо-языке, мне все еще нужно встретить его применимость на моей нынешней работе.

Может быть, в этом нет необходимости.

2 голосов
/ 08 июня 2010

Спасибо Донроби за его ответ выше - я просто расширяю его.

interface Alpha
interface Beta
interface Gamma extends Alpha, Beta
class A {
    public void f(Alpha a)
    public void f(Beta b)
}
class B extends A {
    public void f(Object o) {
        super.f(o); // What happens when o implements Gamma?
    }
}

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

1 голос
/ 10 июня 2010

Благодаря ответам Донроби и Дэвида, я понимаю, что основная проблема с введением контраст-дисперсии параметров - это интеграция с механизмом перегрузки .

Так что не толькопроблема с одним переопределением для нескольких методов, но также и другим способом:

class A {
    public void f(String s) {...}
}

class B extends A {
    public void f(String s) {...} // this can override A.f
    public void f(Object o) {...} // with contra-variance, so can this!
}

И теперь есть два допустимых переопределения для одного и того же метода:

A a = new B();
a.f(); // which f is called?

Кроме проблем с перегрузкойЯ не мог думать ни о чем другом.

Редактировать: С тех пор я нашел эту запись C ++ FQA (20.8) , которая согласуется с вышеизложенным - наличиеперегрузки создает серьезную проблему для противоречивости параметров.

...