Почему нет противопоставления параметров для переопределения?

StackOverflow https://stackoverflow.com/questions/2995926

Вопрос

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?
}
Это было полезно?

Решение

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

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

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, поскольку подпись идеальная совпада. После того, как вы добавите Contravariance на промежуточный уровень, проблема в том, что есть аргументы, которые действительны в 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 принимает 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. Анкет Но это только означает, что компилятор не найдет скрытый перегрузить по умолчанию, а не то, чтобы его не было:

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

Другие советы

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

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

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

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

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

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

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

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

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

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) напрямую, вы получите ошибку компиляции.)

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top