На чистом выпуске противопоказанием дисперсии
Добавление противопоказаний к дисперсии языка открывает целый много потенциальных проблем или нечистых решений и предложений очень мало преимуществ, как он может быть легко моделируется без поддержки языка:
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&)
}
Практически не используется для разнотипных типов параметров. – fredoverflow
... и простые обходные пути, такие как объявление метода с исходной сигнатурой, которая переадресует вызов модифицированной сигнатуре. –