Подтвердить что ты не робот

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

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

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

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

class IWriter {
public:
  virtual ~IWriter() {}

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

Ответ 2

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

Собственно, нет, это очень неестественно.

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

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

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

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

Решение № 1 MyWriter становится виртуальным. Последствия: весь существующий код С++ в мире становится легко прорваться через опечатку или конфликт имен. Первоначально метод MyWriter не должен был переоцениваться, поэтому внезапное превращение его в виртуальную волю (закон murphy) нарушает основную логику класса MyWriter, когда кто-то происходит из MyOwnClass. Это означает, что внезапное создание MyWriter:: write virtual - плохая идея.

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

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

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

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

Ответ 3

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

Однако расширения С++ 11 приближают нас. Рассмотрим это:

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

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

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

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

Ответ 4

Почему С++ не делает этого?

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

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

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

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

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

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

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

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

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

Более того, прямая реализация виртуальных вызовов v-table уже невозможна. Самый быстрый способ виртуальной диспетчеризации - это не соответствующая реализация. Комитет С++ не собирается делать какие-либо изменения, которые сделали бы невозможным такие реализации.

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

Ответ 5

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