Термин «интерфейс» в C ++

11

Ява проводит четкое различие между classи interface. (Я верю, что C # тоже, но у меня нет опыта с этим). Однако при написании C ++ не существует языкового различия между классом и интерфейсом.

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

Я всегда склонялся к подходу «писать вещи самым очевидным образом», поэтому, если в C ++ у меня есть то, что можно назвать интерфейсом в Java, например:

class Foo {
public:
  virtual void doStuff() = 0;
  ~Foo() = 0;
};

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

class Foo {
public:
  virtual void doStuff() = 0;
  ~Foo() {}
protected:
  // If it needs this to do its thing:
  int internalHelperThing(int);
  // Or if it doesn't need the this pointer:
  static int someOtherHelper(int);
};

Что делает этот интерфейс больше не интерфейсом в смысле Java.

Вместо этого в C ++ есть две важные концепции, связанные с одной и той же проблемой наследования:

  1. virtual inhertiance
  2. Классы без переменных-членов не могут занимать дополнительное место при использовании в качестве базы

    «Подобъекты базового класса могут иметь нулевой размер»

    Ссылка

Из тех, кого я стараюсь избегать №1, везде, где это возможно - редко можно встретить сценарий, где это действительно самый «чистый» дизайн. # 2, однако, является тонким, но важным различием между моим пониманием термина «интерфейс» и особенностями языка C ++. В результате этого я в настоящее время (почти) никогда не называю вещи «интерфейсами» в C ++ и говорю о базовых классах и их размерах. Я бы сказал, что в контексте C ++ «интерфейс» является неправильным.

Хотя до меня дошло, что не так много людей делают такое различие.

  1. Стоит ли мне что-то терять, позволяя (например protected) не- virtualфункциям существовать в «интерфейсе» в C ++? (Мое чувство как раз наоборот - более естественное место для общего кода)
  2. Является ли термин «интерфейс» значимым в C ++ - подразумевает ли он только чистый virtualили было бы справедливо называть классы C ++ без переменных-членов интерфейсом по-прежнему?
Flexo
источник
C # обладает тем же множественным наследованием интерфейсов, единичным наследованием реализации, что и Java, но благодаря использованию универсальных методов расширения их internalHelperThingможно смоделировать почти во всех случаях.
Sjoerd
Обратите внимание, что выставлять виртуальных участников - плохая практика.
Klaim
публика ~Foo() {}в абстрактном классе является ошибкой (почти) при любых обстоятельствах.
Вольф

Ответы:

11

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

Что касается вашего примера: в Java (и аналогично в C #) ваш код, вероятно, будет подразумевать разделение задач:

interface IFoo {/*  ... */} // here is your interface

class FooBase implements IFoo 
{
     // make default implementations for interface methods
}

class Foo extends FooBase
{
}

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

Док Браун
источник
3
Другое возможное значение «интерфейса» для программиста на C ++ - это publicчасти .hфайла.
Дэвид Торнли
На каком языке пример вашего кода? Если это попытка быть C ++, я собираюсь заплакать ...
Qix - MONICA БЫЛА НЕПРАВИЛЬНОЙ
@Qix: будьте проще, прочитайте мою публикацию еще раз (там четко указано, что код написан на Java), и, если вы набрали> 2000 баллов, вы можете отредактировать мою публикацию, добавив эквивалентный пример кода C ++, чтобы сделать мой пример более Чисто.
Док Браун
Если это Java, то все равно это не так, и нет, я не могу его отредактировать; недостаточно символов для изменения. Java не использует двоеточия при реализации / расширении, поэтому мне было интересно, была ли это попытка C ++ ...
Qix - MONICA БЫЛА НЕПРАВИЛЬНОЙ
@Qix: если вы найдете больше синтаксических проблем, вы можете оставить их в качестве рождественского подарка :-)
Док Браун
7

Это звучит немного похоже на то, что вы, возможно, попали в ловушку путаницы смысла того, что интерфейс является концептуально как реализацией (интерфейс - строчная буква «i»), так и абстракцией (интерфейс - прописная буква «I»). ).

Что касается ваших примеров, ваш первый кусочек кода - просто класс. В то время как у вашего класса есть интерфейс в том смысле, что он предоставляет методы для обеспечения доступа к его поведению, он не является интерфейсом в смысле объявления интерфейса, предоставляющего уровень абстракции, представляющий тип поведения, которому вы, возможно, захотите, чтобы классы осуществлять. Ответ Дока Брауна на ваш пост показывает вам, о чем я здесь говорю.

Интерфейсы часто рекламируются как «обходной путь» для языков, которые не поддерживают множественное наследование, но я чувствую, что это скорее заблуждение, чем жесткая истина (и я подозреваю, что я могу быть огорчен этим утверждением!). Интерфейсы действительно не имеют ничего общего с множественным наследованием, так как они не требуют наследования или предков для обеспечения функциональной совместимости между классами или абстракции реализации для классов. На самом деле, они могут позволить вам эффективно покончить с наследованием полностью, если вы захотите реализовать свой код таким образом - не то, чтобы я полностью рекомендовал его, но я просто говорю, что вы могли бысделай это. Таким образом, реальность такова, что независимо от вопросов наследования интерфейсы предоставляют средства, с помощью которых можно определять типы классов, устанавливая правила, определяющие, как объекты могут взаимодействовать друг с другом, что позволяет вам определять поведение, которое ваши классы должны поддерживать без диктовать конкретный метод, используемый для реализации этого поведения.

Стоит ли мне что-то терять, позволяя (например, защищенным) не виртуальным функциям существовать в «интерфейсе» в C ++? (Мое чувство как раз наоборот - более естественное место для общего кода)

Чистый Интерфейс должен быть полностью абстрактным, поскольку он позволяет определить контракт совместимости между классами, которые не обязательно унаследовали свое поведение от общего предка. При реализации вы хотите сделать выбор о том, разрешить ли расширение реализации в подклассе. Если ваши методы не таковы virtual, вы потеряете возможность расширять это поведение позже, если решите, что вам нужно создать классы-потомки. Независимо от того, является ли реализация виртуальной или нет, Интерфейс определяет поведенческую совместимость сам по себе, в то время как класс обеспечивает реализацию этого поведения для экземпляров, которые представляет класс.

Является ли термин «интерфейс» значимым в C ++ - подразумевает ли он только чисто виртуальный или было бы справедливо называть классы C ++ без переменных-членов интерфейсом по-прежнему?

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

S.Robins
источник
1
+1 за различие между «иметь» и «быть» интерфейсом.
Док Браун
6

Java-интерфейсы не являются «обходным путем», они представляют собой обдуманное проектное решение, позволяющее избежать некоторых проблем с множественным наследованием, таких как наследование алмазов, и поощрять методы проектирования, минимизирующие связывание.

Ваш интерфейс с защищенными методами - это учебник, в котором нужно «предпочесть композицию наследованию». Чтобы процитировать Саттера и Александреску в разделе под этим именем из их превосходных стандартов кодирования C ++ :

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

Включив в свой интерфейс вспомогательные функции, вы можете сэкономить немного времени на наборе текста, но вы вводите связь, которая повредит вам в будущем. Почти всегда лучше в долгосрочной перспективе разделить вспомогательные функции и передать их в Foo*.

STL является довольно хорошим примером этого. Как можно больше вспомогательных функций извлекаются <algorithm>вместо того, чтобы находиться в классах контейнера. Например, поскольку sort()для своей работы используется API открытого контейнера, вы знаете, что можете реализовать собственный алгоритм сортировки без необходимости изменения кода STL. Такая конструкция позволяет использовать такие библиотеки, как boost, которые улучшают STL вместо его замены, и STL не нужно ничего знать о boost.

Карл Билефельдт
источник
2

Стоит ли мне что-то терять, позволяя (например, защищенным) не виртуальным функциям существовать в «интерфейсе» в C ++? (Мое чувство как раз наоборот - более естественное место для общего кода)

Вы могли бы подумать об этом, но размещение защищенного, не виртуального метода в другом абстрактном классе диктует реализацию любому, кто пишет подкласс. Это наносит ущерб цели интерфейса в его чистом смысле, то есть обеспечивает облицовку, которая скрывает то, что находится под ним.

Это один из тех случаев, когда нет универсального ответа, и вы должны использовать свой опыт и суждения для принятия решения. Если вы можете со 100% уверенностью сказать, что каждый возможный подкласс вашего полностью виртуального класса Fooвсегда будет нуждаться в реализации защищенного метода bar(), тогда Fooэто подходящее место для него. Если у вас есть подкласс Baz, который вам не нужен bar(), вы Bazдолжны либо согласиться с тем фактом, что у него будет доступ к коду, которого он не должен, либо выполнить перестройку иерархии классов. Первое не является хорошей практикой, а второе может занять больше времени, чем те несколько минут, которые потребовались бы для того, чтобы правильно все наладить.

Является ли термин «интерфейс» значимым в C ++ - подразумевает ли он только чисто виртуальный или было бы справедливо называть классы C ++ без переменных-членов интерфейсом по-прежнему?

Раздел 10.4 стандарта C ++ упоминает об использовании абстрактных классов для реализации интерфейсов, но не определяет их формально. Этот термин имеет смысл в общем контексте информатики, и любой компетентный должен понимать, что « Fooинтерфейс для (чего бы то ни было)» подразумевает некоторую форму абстракции. Те, кто знаком с языками с определенными интерфейсными конструкциями, могут думать, что они чисто виртуальные, но любой, кому действительно нужно работать Foo, посмотрит на его определение, прежде чем продолжить.

Blrfl
источник
2
В чем разница между предоставлением чисто виртуальной декларации и декларации плюс реализация? В обоих случаях должна быть реализация, и bazвсегда может быть реализация, подобная return false;или любая другая. Если метод не применяется ко всем подклассам, он не принадлежит базовому абстрактному классу ни в какой форме.
Дэвид Торнли
1
Я думаю, что в вашем последнем предложении говорится, что мы находимся на одной странице: нет причин, по которым вы не можете иметь защищенные методы в абстрактных классах, но они не должны быть в дереве наследования выше, чем это абсолютно необходимо. Я просто думаю, что если классу нужно придумать реализацию, он не в нужном месте дерева.
Blrfl