пятница, 3 декабря 2010 г.

Вопрос об интерфейсах

Похоже, что я нашёл самый сложный вопрос для собеседований по C++ - полностью правильного ответа ещё ни разу не слышал, а ведь не junior'ов собеседую...
Звучит он приблизительно так:
Есть такие понятия в ООП как "наследование реализации" и "наследование интерфейса"... Как Вы понимаете "наследование интерфейса" и есть ли оно в C++?

Т.е. вопрос вроде бы простой...
И вот самое странное, что почти у всех кандидатов при ответе обнаруживается некая каша из ключевого слова interface, понятия абстрактного класса, понятия класса реализующего некий интерфейс, собственно понятия интерфейса и иногда даже COM-интерфейса. Т.е. каждый приходит со своими ингридиентами к каше :) В таких условиях ожидать идеально правильного ответа невозможно, зато это позволяет "залезть в голову" кандидату.

Основа вопроса - это базисные вещи в C++, о которых часто просто не задумываются. Потому и ставлю этот вопрос в открытой форме, чтобы понять: что человек знает, как умеет думать и делать выводы из того что уже знает. 

 
А теперь доберёмся до приблизительно правильного ответа как его вижу я.

Итак, суть терминов, которые используются при ответе:
1) Интерфейс(почти классическое определение этого понятия, нагло сцитированное из wikipedia) - это некий перечень всех функций, которые может выполнить элемент, который "соответствует" этому интерфейсу. При этом в понятие "интерфейс" включены сигнатуры функций/методов, но интерфейс не оговаривает способы реализации этих методов. Т.е. интерфейс описывает некую границу, контракт, по которому могут взаимодействовать объекты расположенные по разные стороны от границы.
2) Класс, реализующий интерфейс - это класс, который предоставляет реализацию функций определённых в интерфейсе.
3) Абстрактный родительский класс - это базовый класс, у которого часть функций не реализована(pure virtual) и предоставлена для уточнения в классах-потомках.
4) Ключевое слово interface - это маппинг на ключевое слово struct, причём MS specific маппинг.
5) Понятие COM-интерфейса наиболее близко к понятию интерфейса. Разве, что есть набор обязательных функций, которые надо включать в любой COM-интерфейс, но об этом обычно забывают упомянуть.
 
Из №1 и формулировки вопроса(наследование реализации и наследование интерфейса разделены) получаем, что наследование интерфейса - это механизм языка, который позволит определить условия контракта и никак не повлиять на реализацию.

Можно подумать, что в C++ для получения интерфейса достаточно определить ВСЕ функции в базовом абстрактном классе, как pure virtual(т.е. получить pure abstract class). Но в C++ существуют функции, которые ВСЕГДА есть у класса - это те самые функции, которые часто реализуются компилятором по умолчанию - т.е. мы говорим о конструкторах с деструкторами. А значит, классы-потомки получат от класса-предка реализации этих функций (UPD: тут я не раскрыл мысль: есть конструкторы и деструктор, и класс-потомок вызовет у себя реализацию конструктора/деструктора класса предка не за счёт наследования, а за счет стандарта языка, который гарантирует вызов контруктора/деструктора класса-предка из контруктора/деструктора класса-потомка), - что говорит о том, что мы не получаем в этом случае "наследование интерфейса". :)

Структуры же в C++ также обладают конструкторами и деструкторами, но об этом часто забывают. Т.е. мы снова приходим к выводам из абзаца выше.
 
Как вывод: 
Вот и не осталось средств реализовать в C++ чистое "наследование интерфейса", и есть у нас наследование реализации.

P.S. 
Зачем я задаю такой вопрос? Так структурное понимание интерфейса позволяет хорошо справляться с написанием шаблонов. Это ведь отсутствие возможности предоставить только "наследование интерфейса" и проверку на соответствие интерфейсу было одной из основных причин придумывать такие механизмы как concepts для нового стандарта - понимаю, что сырая вещь, но так жалко что не включили :( .

10 комментариев:

  1. Правильный вывод - в С++ таки есть "наследование интерфейса", но с оговорками. Неправильно после статьи было бы все наследование грести под одну гребенку, т.е. "наследование реализации". Только больше путаницы будет.
    Конструкторы и деструкторы - вещь специфическая, а в pure abstract class - даже тривиальная.

    К тому же, хотелось бы практический пример, в котором бы ярко было показано, что "ой как жалко, что в С++ нет наследование интерфейса".

    :)

    ОтветитьУдалить
  2. "в С++ таки есть "наследование интерфейса", но с оговорками" - если говорить с оговорками, то их(оговорки) надо определять... :)

    Как пример - пустые интерфейсы схожие по идее с маркер-интерфейсами из Java. Отдельно такой функциональности в c++ нет и чистого наследования интерфейса нет, а потому мы обязаны предоставлять пустое определение интерфейса с виртуальным деструктором и телом без единого значащего оператора, - просто чтобы заработало и мы могли корректно работать\удалять объект через вызовы delete. Т.е. мы оказываемся ДОЛЖНЫ предоставить реализацию, которую смогут использовать потомки.

    ОтветитьУдалить
  3. Неудачный пример. В яве как и в шарпе не нужно явно писать деструктор и, тем более, объявлять его виртуальным. Он есть, но он спрятан. В С++ его нужно явно написать. Тем более в маркер интерфейсе его тело будет пустым.

    much ado about nothing :)

    Я согласен с твоими выводами, я не согласен с критичность этой проблемы. Давай другой пример.

    ОтветитьУдалить
  4. В интерфейсе тело деструктора должно быть пустым. Но это "должно быть пустым" проистекает из того, что на c++ нет "наследования интерфейса", а не из того что мы хотели его сделать пустым. А это разные вещи. Будь на C++ "наследование интерфейса нам бы вообще не было необходимости определять деструктор.

    Саш, и прочитай "что такое маркер-интерфейсы" - твоя реплика к ним не относится. ;) Без наличия в языке чистой реализации "наследования интерфейса" - реализовать эту фичу невозможно. А тела конструктора/деструктора (пусть даже и созданные компилятором по умолчанию) не помогает с решением этой проблемы.

    И я не говорил, что эта проблема критична :) Ты запросил пример - я привел пример, который показывает что отсутствие "наследования интерфейса" приводит к дополнительным пляскам. Пусть в данном случае они относительно минимальны, но они есть. А если сам подумаешь, то найдёшь ещё кучу таких моментов...

    И повторюсь - я этот вопрос задаю не на "правильный ответ", а на "понять как думает"

    ОтветитьУдалить
  5. Насчет почитать...а в чем проблема? Эта фича в яве называется маркер-интерфейс, в С++ ее нет, но реализовать маркер-интерфейс паттерн можно, хоть и кривовато выглядеть будет. Не придирайся к названиям ;)

    По-моему, проблема с реализацией и использованием маркер-интерфейсов в С++ еще и в другом. Наличие маркера проверяется с помощью dynamic_cast (может можно и по другому, я не разбирался). А так как dynamic_cast работает только с полиморфными типами, то нельзя проверить наличие маркера у неполиморфного класса (compile error C2683).

    В яве/шарпе этой проблемы нет.

    Я вот подумал, может потому то и нельзя наследовать только интерфейс в С++ потому что для этого нужно сделать как в шарпе: чтобы все классы были полиморфными, наследовались от какого-то System.Object и т.д.?

    Со всем остальным согласен.

    ОтветитьУдалить
  6. Хммм... Возможно...
    Надо будет посмотреть есть ли что-то подобное в Qt - у них ведь все объекты производные от QObject, так что могли что-то подобное могли и предоставить.

    ОтветитьУдалить
  7. Хммм... Возможно...
    Надо будет посмотреть есть ли что-то подобное в Qt - у них ведь все объекты производные от QObject, так что могли что-то подобное могли и предоставить.

    ОтветитьУдалить
  8. В интерфейсе тело деструктора должно быть пустым. Но это "должно быть пустым" проистекает из того, что на c++ нет "наследования интерфейса", а не из того что мы хотели его сделать пустым. А это разные вещи. Будь на C++ "наследование интерфейса нам бы вообще не было необходимости определять деструктор.

    Саш, и прочитай "что такое маркер-интерфейсы" - твоя реплика к ним не относится. ;) Без наличия в языке чистой реализации "наследования интерфейса" - реализовать эту фичу невозможно. А тела конструктора/деструктора (пусть даже и созданные компилятором по умолчанию) не помогает с решением этой проблемы.

    И я не говорил, что эта проблема критична :) Ты запросил пример - я привел пример, который показывает что отсутствие "наследования интерфейса" приводит к дополнительным пляскам. Пусть в данном случае они относительно минимальны, но они есть. А если сам подумаешь, то найдёшь ещё кучу таких моментов...

    И повторюсь - я этот вопрос задаю не на "правильный ответ", а на "понять как думает"

    ОтветитьУдалить
  9. Неудачный пример. В яве как и в шарпе не нужно явно писать деструктор и, тем более, объявлять его виртуальным. Он есть, но он спрятан. В С++ его нужно явно написать. Тем более в маркер интерфейсе его тело будет пустым.

    much ado about nothing :)

    Я согласен с твоими выводами, я не согласен с критичность этой проблемы. Давай другой пример.

    ОтветитьУдалить
  10. Правильный вывод - в С++ таки есть "наследование интерфейса", но с оговорками. Неправильно после статьи было бы все наследование грести под одну гребенку, т.е. "наследование реализации". Только больше путаницы будет.
    Конструкторы и деструкторы - вещь специфическая, а в pure abstract class - даже тривиальная.

    К тому же, хотелось бы практический пример, в котором бы ярко было показано, что "ой как жалко, что в С++ нет наследование интерфейса".

    :)

    ОтветитьУдалить