Подтвердить что ты не робот

Почему нет параметра contra-variance для переопределения?

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

Тем не менее, поддерживайте contra-variance в типах параметров - вместо этого он переводит на более загрузку (Java) или скрывает (С++).

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

Пример (Java):

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

Ответ 1

О чистом вопросе о противоречивости

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

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& ) не обязательно должен быть виртуальным, и вызов полностью соответствует запрещению виртуального механизма отправки.

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

// 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] система будет называть не лучшим совпадением аргументы.

Конечно, можно утверждать иначе: 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 принимает a B&, а не a A& как аргумент! Фактическое переопределение, которое было бы на месте, не сможет обрабатывать аргумент a, даже если подпись метода в месте вызова кажется совершенно правильной. Этот путь позволяет нам определить, что второй путь намного хуже первого. R::f( B& ) не может быть переопределением Q::f( A& ).

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

В режиме перегрузки и скрытия

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

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

Ответ 2

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?
}

Ответ 3

В С++ Stroustrup обсуждает причины для краткого скрытия в разделе 3.5.3 Проект и эволюция С++. Его рассуждение (я перефразирую), что другие решения поднимают столько же вопросов, и так было с C С дней классов.

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

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

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

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

Ответ 4

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

Возможно, в этом нет необходимости.

Ответ 5

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

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?
    }
}

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

Ответ 6

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

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

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?

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

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