Почему C ++ не позволяет базовым классам реализовывать наследуемый интерфейс производного класса?

Вот о чем я говорю

<code>// some guy wrote this, used as a Policy with templates
struct MyWriter {
  void write(std::vector<char> const& data) {
    // ...
  }
};
</code>

В каком-то существующем коде люди использовали не шаблоны, а интерфейсы + стирание типа

<code>class IWriter {
public:
  virtual ~IWriter() {}

public:
  virtual void write(std::vector<char> const& data) = 0;
};
</code>

Кто-то еще хотел использовать его с обоими подходами и пишет

<code>class MyOwnClass: private MyWriter, public IWriter {
  // other stuff
};
</code>

MyOwnClass реализован в терминах MyWriter. Почему унаследованные функции-члены MyOwnClass не реализуют интерфейс IWriter автоматически? Вместо этого пользователь должен написать функции пересылки, которые ничего не делают, кроме как вызывают версии базового класса, как в

<code>class MyOwnClass: private MyWriter, public IWriter {
public:
  void write(std::vector<char> const& data) {
    MyWriter::write(data);
  }
};
</code>

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

Почему C ++ не делает это? Кажется, это естественно.

 SigTerm05 мая 2012 г., 19:39
не виртуальные методы.
 Konrad Rudolph05 мая 2012 г., 20:42
@ fontanini Он просит разъяснений. Я все еще не уверен, против чего вы возражаете. Учтите, что Йоханнес более или менее вдыхал стандарт C ++ и является одним из тех, кто обладает самыми полными знаниями по C ++ на этой доске, или даже в Интернете. Когдао @ есть вопрос, он хороший, и его нелегко отбросить.
 Bo Persson05 мая 2012 г., 19:39
Потому что происходит отstruct не показывает того же намерения, что и производное отinterface?
 Ben Voigt05 мая 2012 г., 19:44
Безvirtual функции, чистые спецификации и т. д., в этом нет никакого смысла. Перестаньте пытаться вырезать и вставлять код Java в C ++ и подумайте, что он будет работать. Литб Я ожидаю, что знаю лучше, чем это.
 SigTerm05 мая 2012 г., 20:06
@ JohannesSchaub-litb: Возможно, в другой раз. Извините, но на данный момент у меня нет впечатления, что вам действительно нужна помощь (возможно, я ошибаюсь) из-за вашей репутации и заявления "иногда я троллю" в профиле. И сейчас я не нахожу это обсуждение особенно интересным. Предлагаю проверить C ++ FAQ. Хорошего дня

Ответы на вопрос(5)

Я не уверен, что ты здесь спрашиваешь.Мо C ++ переписать, чтобы разрешить это? Да, но с какой целью?

Потому чтоMyWriter а такжеIWriter - это совершенно разные классы, в C ++ запрещено называть членMyWriter через экземплярIWriter. Указатели-члены имеют совершенно разные типы. И так же, какMyWriter* не конвертируется вIWriter*, тоже неvoid (MyWriter::*)(const std::vector<char>&) конвертируется вvoid (IWriter::*)(const std::vector<char>&).

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

Помните: функции-члены всегда принимают дополнительный параметр: athis указатель на объект, на который они указывают. Вы не можете позвонитьvoid (MyWriter::*)(const std::vector<char>&) наIWriter*. Третий класс может иметь метод, который преобразуется в соответствующий базовый класс, но на самом деле он должен есть этот метод. Так что либо вы, либо компилятор C ++ должны его создать. Правила C ++ требуют этого.

Подумайте, что должно произойти, чтобы это работало без метода производного класса.

Функция получаетIWriter*. Пользователь называетwrite член, использующий ничего кромеIWriter* указатель Итак ... как именно компилятор может сгенерировать код для вызоваMyWriter::writer? Помнить:MyWriter::writer Потребности a MyWriter экземпляр. И нет никакой связи междуIWriter а такжеMyWriter.

Так как именно компилятор может выполнять приведение типов локально? Компилятор должен проверить виртуальную функцию, чтобы узнать, принимает ли фактическая функция, которая будет вызвана,IWriter или другой тип. Если он принимает другой тип, ему придется преобразовать указатель в его истинный тип, а затем выполнить другое преобразование в тип, необходимый для виртуальной функции. После всего этого он сможет сделать звонок.

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

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

Даже хуже, прямая реализация виртуальных вызовов в виртуальной таблице больше невозможна. Самый быстрый способ сделать виртуальную диспетчеризацию не будет соответствующей реализацией. Комитет C ++ являетсян собираюсь внести любое изменение, которое сделает такие реализации невозможными.

Снова, с какой целью? Просто чтобы вам не пришлось писать простую функцию пересылки?

 Johannes Schaub - litb05 мая 2012 г., 23:05
В этом случае язык также допускает кросс-кейсA вB хотя ни один из классов «не является прямым родителем / дочерним родственником друг друга». Но объекты обоих классов отображаются как подобъекты одного законченного объекта:struct A { virtual ~A() {}}; struct B { virtual ~B() {} void f() {}}; struct C : A, B {}; int main() { A *a = new C; dynamic_cast<B*>(a)->f(); }. В моем случае оба класса отображаются как базовые классы одного производного класса.
 Ben Voigt06 мая 2012 г., 05:18
@ litb: В этом случае язык допускает перекрестное приведение? Я думал, что оба типа должны быть полиморфными, иMyWriter нет. Кроме,dynamic_cast дорогой
 Nicol Bolas05 мая 2012 г., 23:11
@ Johannes: "Может ли компилятор сделать это для меня?" Да, это возможно. Но это не сделало бы это неявно, так что вам все еще нужнонескольк синтаксис там, чтобы сказать компилятору сделать это. А так как для этого синтаксиса нужна подпись функции, а также имя (и, вероятно, подпись) o, для вызываемой функции ... на самом деле это просто небольшой кусочек синтаксического сахара для написания функции пересылки самостоятельно. Не стесняйтесь предлагать это Комитету по стандартизации C ++ 1y. Я не ожидал бы, что это получит большую тягу, хотя, учитывая, сколько углового случая это.
 Johannes Schaub - litb05 мая 2012 г., 23:02
Я не спрашиваю буквально о возможности напрямую звонитьMyWriter::write позвонивIWriter::write. Я просто не хочу помещать определение функции вMyOwnClass это только впередMyWriter::write. Может ли компилятор не сделать это для меня? Все, что меня интересует, это: результирующий объект может быть настроен так, что вызовIWriter::write оказываетс звонитMyWriter::write. Между ними могут быть батуты и еще много чего (все, что нужно, чтобы это работало), которые в конечном итоге делают последний вызовMyWriter::write.

На самом деле нет, это очень неестественно.

Пожалуйста, обратите внимание, что мои рассуждения основаны на моем собственном понимании "здравого смысла" и в результате могут привести к существенным ошибкам.

Вы видите, у вас есть два разных метода, первый в MyWriter, который не является виртуальным, и второй в IWriter, который является виртуальным. Они совершенно разные, несмотря на то, что выглядят похожим

Предлагаю проверитьэтот вопро. Хорошая вещь о не виртуальных методах состоит в том, что независимо от того, что вы делаете, если они не вызывают виртуальные методы, их поведение никогда не изменится. То есть кто-то, производный от вашего класса не виртуальными методами, не нарушит существующий метод, маскируя их. Виртуальные методы предназначены для переопределения. Цена этого заключается в том, что можно сломать основную логику, ненадлежащим образом переопределяя виртуальный метод. И это корень вашей проблемы.

Допустим, то, что вы предлагаете, разрешено. (автоматическое преобразование в виртуальное с множественным наследованием) Есть два возможных решения:

Solution # 1 MyWriter становится виртуальным. Последствия: весь существующий в мире код C ++ становится легко сломать с помощью опечатки или столкновения имен. Метод MyWriter изначально не должен был быть переопределен, поэтому внезапно превращение его в виртуальную волю (закон Мерфи) нарушает основную логику класса MyWriter, когда кто-то наследует MyOwnClass. А это значит, что внезапно сделать MyWriter :: write виртуальным - плохая идея.

Soluion # 2 MyWriter остается статическим, но временно он включен как виртуальный метод в IWriter, пока не будет переопределен. На первый взгляд беспокоиться не о чем, но давайте подумаем. IWriter реализует какую-то концепцию, которую вы имели в виду, и она должна что-то делать. MyWriter реализует другую концепцию. Чтобы назначить MyWriter :: write как метод IWriter :: write, вам нужны две гарантии:

Compiler должен гарантировать, что MyWriter :: write делает то, что должен делать IWriter :: write ().Compiler должен гарантировать, что вызов MyWriter :: write из IWriter не нарушит существующую функциональность в коде MyWriter, который программист ожидает использовать в другом месте.

Так вот, дело в том, что компилятор не может этого гарантировать. Функции имеют похожее имя и список аргументов, но по закону Мерфи это означает, что они, вероятно, делают совершенно разные вещи. (например, sinf и cosf имеют один и тот же список аргументов), и маловероятно, что компилятор сможет предсказать будущее и убедиться, что ни на одном этапе разработки MyWriter не будет изменен таким образом, что он станет несовместимым с IWriter. Таким образом, поскольку машина сама по себе не может принять разумное решение (для этого не нужен ИИ), она должна спросить ВАС, программиста: «Что вы хотите сделать?». И вы говорите: «перенаправьте виртуальный метод в MyWriter :: write (). Это совершенно ничего не сломает. Я думаю.».

И поэтому вы должны указать, какой метод вы хотите использовать вручную ....

 Johannes Schaub - litb06 мая 2012 г., 10:02
Спасибо за Ваш ответ. У меня есть несколько вопросов. Msgstr "Компилятор должен убедиться, что MyWriter :: write делает то, что должен делать IWriter :: write ()." Почему это должен гарантировать компилятор, а не программист? Программист - это тот, кто получил MyOwnClass из интерфейса и MyWriter, поэтому он лучше проверяет, что MyWriter удовлетворяет IWriter.
 Johannes Schaub - litb06 мая 2012 г., 10:04
Я вижу то же самое с этим:struct A { void nonVirtualNastiness() {} virtual void bark() { } }; тогда авторA решает извлечь из интерфейсаstruct A : public IDoggy { virtual void bark() { } };. В настоящее времяIDoggy::bark слишком автоматически отменяетсяA::bark и компилятор доверяет программисту, что он проверил их совместимость. А такжеnonVirtualNastiness может внезапно стать виртуальным автоматически благодаря наследованию, потому что базовый класс имеет его виртуальным.
 SigTerm06 мая 2012 г., 11:25
@ JohannesSchaub-litb: «Почему это должен гарантировать компилятор, а не программист?» Если компилятор автоматически назначит метод из MyClass как виртуальную имплементацию IWriter, то это необходимо будет обеспечить. Если компилятор не может гарантировать, то компилятору нужно будет сделать предположение о внутренней логике программы, и по закону Мерфи любое автоматическое предположение будет неверным. Поэтому имеет смысл попросить информацию у программиста. По моему опыту, хорошая автоматическая система, предназначенная для опытных пользователей, никогда не должна делать каких-либо предположений - она должна запрашивать решение пользователя. (Продолжение)
 SigTerm06 мая 2012 г., 11:31
@ JohannesSchaub-litb: .. (продолжение) такое поведение (делает именно так, как вы сказали) дает пользователю полный контроль, но за счет случайного выполнения вредоносной команды. Насколько я могу судить, C ++ следует режиму «опытного пользователя», и мне нравится этот способ - именно программист должен принимать решения. Конечно, это вопрос личных предпочтений. Чтобы быть справедливым, все дело вкуса и субъективного мнения. Мне комфортно с языком, работающим как есть, и я не вижу большой пользы от реализации схемы, которую вы упомянули.
 SigTerm06 мая 2012 г., 11:29
@ JohannesSchaub-litb: (продолжение) с текущим программистом внедрения - это тот, который гарантирует, что все работает, как ожидается, и программист должен решить. Это похоже на дистрибутивы Linux - «удобный» дистрибутив (например, Ubuntu) будет предполагать, что пользователь не всегда знает, что он делает, и может молча выбрать какой-либо действие / конфигурацию, что может раздражать пользователя LOT. Дистрибутив для опытных пользователей никогда не помешает вам, никогда ничего не решит и всегда будет делать то, что вы Спросил это делать. (Продолжение)

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

Звонокwrite в выражении со статическим типомMyBigClassоэтому @ будет неоднозначно относительно того, какая из унаследованных функций желательна.

Еслиwrite вызывается только через указатели базового класса, а затем определяетсяwrite в производном классе не требуется, вопреки утверждению в вопросе. Теперь, когда вопрос изменился и теперь включает чистый спецификатор, необходимо реализовать эту функцию в производном классе, чтобы сделать класс конкретным и инстанцируемым.

MyWriter::write нельзя использовать для механизма виртуальных вызововMyBigClass, потому что механизм виртуального вызова требует функции, которая принимает неявныйIWriter* const this, а такжеMyWriter::write принимает неявноеMyWriter* const this. Требуется новая функция, которая должна учитывать разницу адресов междуIWriter подобъект иMyWriter подобъект.

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

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

 Ben Voigt05 мая 2012 г., 20:07
@ Йоханнес: Нет, «интерфейс» не «реализован». Базовый класс не предоставляет функции соответствияvoid write(IMyWriter* const this, std::vector<char> const&) (используя синтаксис стиля C ++, чтобы показать тип неявногоthis параметр, который используется при вызове виртуальной функции).
 Ben Voigt05 мая 2012 г., 19:57
@ JohannesSchaub-litb: А? Является ли ваш вопрос "Новая реализация в производном классе явно необходима. Разве компилятор не может сгенерировать ее для меня?" или что-то другое
 Ben Voigt05 мая 2012 г., 20:10
@ litb: потому что нет функции с правильной подписью вызова ...
 Johannes Schaub - litb05 мая 2012 г., 19:56
Почему это важно, виртуально ли это или нет? Что мешает ввести правило в C ++ Standard, чтобы сделать дело правильным?
 Johannes Schaub - litb05 мая 2012 г., 20:16
«MyWriter :: write не может быть помещен в виртуальную таблицу MyBigClass, потому что для виртуальной таблицы требуется функция, которая принимает неявный IWriter * const this, а MyWriter :: write принимает неявный MyWriter * const this.». Я не понимаю всего этого. Но также, это очень технический аргумент для базового языковой дизайн вопрос. Дизайн vtables должен следовать дизайну языка, а не наоборот.

автоматическ было бы неинтуитивно и удивительно. C ++ не предполагает, что несколько базовых классов связаны друг с другом, и защищает пользователя от конфликтов имен между его членами, определяя вложенные спецификаторы имен для нестатических членов. Добавление неявных объявлений вMyOwnClass откуда подписиIWriter а такжеMyWriter столкновение противоречит защите имен.

Однако расширения C ++ 11 действительно приближают нас. Учти это

class MyOwnClass: private MyWriter, public IWriter {
public:
  void write(std::vector<char> const& data) final = MyWriter::write;
};

Этот механизм будет безопасным, потому что он выражает, чтоMyWriter не ожидает дальнейших переопределений, и удобно, потому что называет сигнатуру функции, которая будет «присоединена», но не более того. Также,final был бы плохо сформирован, если бы функция не была неявноvirtual, поэтому он проверяет, что подпись соответствует виртуальному интерфейсу.

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

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

устраните вывод IWriter в MyOwnClass и продолжайте жить. Это должно решить проблему и не должно мешать коду шаблона.

Ваш ответ на вопрос