Обоснуйте разницу между выбором виртуальной функции и выбором аргумента по умолчанию при вызове через базовый тип.
Виртуальная функция выбирается динамически по фактическому типу объекта, а аргумент по умолчанию подставляется статически по типу выражения, видимому в месте вызова. Поэтому реализация может быть производной, но значение аргумента по умолчанию — из объявления базового класса.
Механизм виртуальных функций предназначен для реализации полиморфизма: код работает через базовый интерфейс, а поведение определяется фактическим типом объекта во время выполнения. Аргументы по умолчанию решают другую задачу — позволяют сократить запись вызова и задаются на этапе компиляции.
Эти механизмы намеренно независимы. Динамический диспетчер виртуальных функций не пересматривает уже скомпилированные аргументы вызова.
Если базовый и производный классы объявляют виртуальную функцию с аргументом по умолчанию, разработчик может ожидать, что при вызове производственной реализации автоматически будет использовано производное значение. Это ожидание неверно.
Такая ошибка особенно опасна при изменении поведения производного класса: вызов через базовый интерфейс может передать значение, предназначенное для базового контракта. В результате программа компилируется, но получает логически несовместимую комбинацию реализации и аргумента.
Сначала компилятор рассматривает статический тип выражения, через которое выполняется вызов. На этом этапе он выбирает подходящую функцию и подставляет отсутствующие аргументы по умолчанию из доступного объявления.
Затем, если функция виртуальная, во время выполнения механизм динамической диспетчеризации выбирает override по фактическому типу объекта. Уже подставленный аргумент при этом не изменяется.
Вызов напечатает Derived: 1: override выбран по фактическому типу Derived, но значение 1 взято из объявления Base, потому что статический тип view — Base&.
Если вызвать метод через выражение типа Derived, будет использовано значение 2. Поэтому одинаковые значения по умолчанию в переопределениях обычно предпочтительнее не объявлять: это уменьшает риск расхождения интерфейсов.
Наиболее безопасный подход — не использовать аргументы по умолчанию у виртуальных функций. Значение можно передавать явно либо вынести вызов с аргументом в невиртуальный интерфейс, который централизованно задаёт политику по умолчанию.
В системе обработки сообщений базовый интерфейс содержит виртуальный метод отправки с параметром тайм-аута по умолчанию. Производный адаптер для локального транспорта задаёт другой тайм-аут, рассчитывая, что он будет применяться при вызове через ссылку на базовый интерфейс.
Вариант с разными значениями по умолчанию прост, но создаёт скрытую зависимость от статического типа вызывающего кода. Вариант с одинаковыми значениями устраняет рассогласование, но не позволяет производному классу переопределить значение таким способом.
Рациональное решение — передавать тайм-аут явно из уровня политики либо использовать невиртуальный метод базового интерфейса, который выбирает значение, а затем вызывает виртуальную операцию. Тогда правило выбора параметра находится в одном месте, а полиморфной остаётся только сама операция.
Изменится ли аргумент по умолчанию, если override вызывается через указатель на производный класс?
Да, в таком случае используется объявление, видимое через статический тип производного указателя. Динамически выбирается реализация, но значение по умолчанию берётся из статического интерфейса выражения.
Можно ли считать аргумент по умолчанию частью виртуального контракта?
Нет. Аргумент по умолчанию не является динамическим свойством функции и не участвует в виртуальном диспетчеризации. Это удобство синтаксиса на стороне вызывающего кода, поэтому оно должно быть согласовано с интерфейсом вручную.
Что произойдёт, если в производном классе изменить только значение по умолчанию, но не сигнатуру функции?
Переопределение останется корректным, если сигнатура совместима и функция действительно переопределяет базовую. Однако разные значения по умолчанию будут зависеть от статического типа места вызова, что может привести к разному поведению для одного и того же объекта. Спецификатор override обнаружит ошибку в переопределении, но не обнаружит логическое рассогласование значений по умолчанию.