2010-06-08 3 views
25

C++ и Java поддерживают ковариацию возвращаемого типа при переопределении методов.Почему нет параметра contra-variance для переопределения?

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

Почему это? Мне кажется, что в этом нет вреда. I может найти одну причину для этого в 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? 
} 
+4

Практически не используется для разнотипных типов параметров. – fredoverflow

+4

... и простые обходные пути, такие как объявление метода с исходной сигнатурой, которая переадресует вызов модифицированной сигнатуре. –

ответ

22

На чистом выпуске противопоказанием дисперсии

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

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], есть прямой призыв к мне mber метод 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&) 
} 
+0

Спасибо, это именно то, что я искал в результате контравариантности. – Oak

+1

Это только первый, о котором я могу думать. Если вы прочитаете «Дизайн и эволюция на C++», вы узнаете, что все дизайнерские решения на этом языке сводятся к: какому преимуществу он предлагает, какие проблемы он создает, может ли он быть реализован с доступными языковыми средствами? Комитет довольно консервативен, и если функция вызывает какие-либо проблемы, если она не дает большого преимущества и не может быть реализована с использованием текущего языка, функция будет отброшена. –

+0

Эй, Дэвид, действительно ли «Дизайн и эволюция C++» на самом деле говорит о контравариантности аргументов функции? Существует раздел о перегрузке релаксации по аргументам (13.7.1), но в нем обсуждается общее неправильное предложение аргумента функции * ковариация *. –

5

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

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

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

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

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

+1

Есть ли у вас пример проблемы, которая возникает? – Oak

+4

Это не похоже на контравариантность ко мне. Если у нас есть 'void A :: copy (A *)' и 'void B :: copy (B *)', и если у нас есть это 'B <: A', то контравариантность была бы, если бы мы имели void B: : copy (A *) 'и' void A :: copy (B *) '. У вас есть ковариация, которая разрешена для типов возврата, но для типов аргументов небезопасна. (Хотя я, возможно, получил это назад - у меня есть склонность переворачивать их вокруг. Но я уверен.) –

+0

@Antal Вопрос OP, о котором конкретно упоминается, скрывается в C++, а код Java в его примере ковариантен. – 2010-06-08 10:32:47

3

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

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

14
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? 
} 
+8

Хорошо, хотя я не вижу причин, по которым он не может переопределить оба, так как каждый вызов 'f' может быть юридически перенаправлен на' B.f'. – Oak

2

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

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

+4

Но 'super.f (o)' должен поднять ошибку типа компиляции: можно статически узнать, что нет 'super.f', который принимает' Object'. (Что произойдет, если вы назвали 'new B() .f (« bad »)', например? Это не просто двусмысленно, а тип-небезопасно!) Существует большой класс небезопасных программ типа, которые возникают, если вы разрешаете неограниченное использование 'super', но вы можете иметь параметрическую контравариантность без этого. –

+0

Ну, я думал, что если мы хотим построить параметр contra-variance, должна быть спецификация 'super' (но в ее нынешнем виде нет спецификации, которая могла бы быть последовательной и полезной). Тогда давайте полностью запретим 'super' в B.f (Object). В чем же суть функции, кроме добавления еще одного метода, связывающего правило с C++? –

0

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

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

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? 

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

Edit: Я с тех пор нашло this C++ FQA entry (20.8) что согласуется с вышесказанным - наличие перегрузки создает серьезную проблему для параметра противопоказания дисперсии.

+0

Это может быть наиболее конкретный (строка в этом случае). –

+0

@devoured Я предполагаю, что это возможно, но это означает введение совершенно нового механизма разрешения на язык - поскольку это не перегрузка. A имеет только один f(), поэтому перегрузка не вступает в игру при вызове f для переменной с типом A. – Oak

+0

Это не новый механизм разрешения, хотя Oak, не так ли? Он будет разрешен точно так же, как при вызове метода в B с помощью String в качестве параметра. Это та же стратегия статического разрешения, которая используется для вызова перегруженных методов, просто применяется в другом контексте. –