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