Двойная отправка выдает предупреждения «скрывает виртуальную функцию», почему? - PullRequest
2 голосов
/ 14 апреля 2011

Я хотел бы реализовать взаимодействия между двумя объектами, типы которых являются производными от общего базового класса. Существует взаимодействие по умолчанию, и определенные вещи могут происходить, когда взаимодействуют объекты одного типа. Это осуществляется с использованием следующей схемы двойной отправки:

#include <iostream>

class A
{
public:
  virtual void PostCompose(A* other)
    {
      other->PreCompose(this);
    }
  virtual void PreCompose(A* other)
    {
      std::cout << "Precomposing with an A object" << std::endl;
    }
};

class B : public A
{
public:
  virtual void PostCompose(A* other) // This one needs to be present to prevent a warning
    {
      other->PreCompose(this);
    }
  virtual void PreCompose(A* other) // This one needs to be present to prevent an error
    {
      std::cout << "Precomposing with an A object" << std::endl;
    }
  virtual void PostCompose(B* other)
    {
      other->PreCompose(this);
    }
  virtual void PreCompose(B* other)
    {
      std::cout << "Precomposing with a B object" << std::endl;
    }
};

int main()
{
  A a;
  B b;
  a.PostCompose(&a); // -> "Precomposing with an A object"
  a.PostCompose(&b); // -> "Precomposing with an A object"
  b.PostCompose(&a); // -> "Precomposing with an A object"
  b.PostCompose(&b); // -> "Precomposing with a B object"
}

У меня есть два, к сожалению, совершенно разных вопроса относительно этого кода:

  1. Как вы думаете, это разумный подход? Вы бы предложили что-то другое?
  2. Если я опускаю первые два метода B, я получаю предупреждения и ошибки компилятора о том, что последние два метода B скрывают методы A. Это почему? Указатель A* не должен быть приведен к указателю B*, или это так?

Обновление : Я только что узнал, что добавление

using A::PreCompose;
using A::PostCompose;

делает ошибки и предупреждения исчезающими, но зачем это нужно?

Обновление 2 : Это аккуратно объяснено здесь: http://www.parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.9, спасибо. Как насчет моего первого вопроса? Любые комментарии по этому подходу?

Ответы [ 3 ]

4 голосов
/ 14 апреля 2011

Двойная диспетчеризация обычно реализуется по-разному в C ++, при этом базовый класс имеет все разные версии (что делает его обслуживающим кошмаром, но таков язык).Проблема с вашей попыткой двойной диспетчеризации состоит в том, что динамическая диспетчеризация найдет наиболее производный тип B объекта, для которого вы вызываете метод, но тогда аргумент имеет статический тип A*.Поскольку A не имеет перегрузки, которая принимает B* в качестве аргумента, то вызов other->PreCompose(this) будет неявно повышен с this до A*, и вы останетесь с единственной отправкой по второму аргументу.

Что касается реального вопроса: почему компилятор выдает предупреждения?почему мне нужно добавить директивы using A::Precompose?

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

Как только найден первый идентификатор, поиск остановится и попытается сопоставить вызов функции с доступными перегрузками, и если вызовне может быть сопоставлено, это вызовет ошибку.Важным моментом здесь является то, что поиск не будет искать дальше в иерархии, если вызов функции не может быть сопоставлен.Добавляя объявление using base::member, вы переносите идентификатор member из базового класса в текущую область.

Пример:

struct base {
   void foo( const char * ) {}
   void foo( int ) {}
};
struct derived : base {
   void foo( std::string const & ) {};
};
int main() {
   derived d;
   d.foo( "Hi" );
   d.foo( 5 );
   base &b = d;
   b.foo( "you" );
   b.foo( 5 );
   d.base::foo( "there" );
}

Когда компилятор встречает выражение d.foo( "Hi" ); статический тип объекта - derived, и поиск проверит все функции-члены в derived, там находится идентификатор foo, и поиск не будет продолжен вверх.Аргументом единственной доступной перегрузки является std::string const&, и компилятор добавит неявное преобразование, поэтому, даже если может быть лучшее потенциальное совпадение (base::foo(const char*) - лучшее совпадение, чем derived::foo(std::string const&) для этого вызова), оно будет эффективноcall:

d.derived::foo( std::string("Hi") );

Следующее выражение d.foo( 5 ); обрабатывается аналогично, поиск начинается с derived и он обнаруживает, что там есть функция-член.Но аргумент 5 не может быть неявно преобразован в std::string const &, и компилятор выдаст ошибку, даже если в base::foo(int) найдется идеальное совпадение.Обратите внимание, что это ошибка в вызове, а не ошибка в определении класса.

При обработке третьего выражения b.foo( "you" ); статический тип объекта равен base (обратите внимание, что фактический объектderived, но тип ссылки base&), поэтому поиск не будет искать в derived, а будет начинаться в base.Он находит две перегрузки, и одна из них соответствует good , поэтому она вызовет base::foo( const char* ).То же самое касается b.foo(5).

Наконец, при добавлении различных перегрузок в наиболее производный класс скрыть перегрузок в базе он не удаляет ихиз объектов, так что вы можете вызвать нужную перегрузку, полностью квалифицировав вызов (который отключает поиск и имеет дополнительный побочный эффект пропуска динамической отправки, если функции были виртуальными), поэтому d.base::foo( "there" ) не будет выполнять поиск ввсе и просто отправьте вызов base::foo( const char* ).

Если бы вы добавили объявление using base::foo в класс derived, вы бы добавили все перегрузки foo в base к доступнымперегрузки в derived, и вызов d.foo( "Hi" ); рассмотрит перегрузки в base и обнаружит, что наилучшей перегрузкой является base::foo( const char* );, поэтому она будет фактически выполняться как d.base::foo( "Hi" );

Во многих случаях разработчики не всегда задумываются о том, как на самом деле работают правила поиска, и может быть удивительно, что вызов d.foo( 5 ); завершается неудачно без объявления using base::foo или, что еще хуже, вызова d.foo( "Hi" );отправляется на derived::foo( std::string const & ), когда оно явно хуже перегрузки, чем base::foo( const char* ).Это одна из причин, по которой компиляторы предупреждают, когда вы скрываете функции-члены.Другая веская причина для этого предупреждения заключается в том, что во многих случаях, когда вы действительно намеревались переопределить виртуальную функцию, вы можете ошибочно изменить сигнатуру:

struct base {
   virtual std::string name() const {
      return "base";
   };
};
struct derived : base {
   virtual std::string name() {        // missing const!!!!
      return "derived";
   }
}
int main() {
   derived d; 
   base & b = d;
   std::cout << b.name() << std::endl; // "base" ????
}

Небольшая ошибка при попытке переопределить 1088 * функция-член name (забывая квалификатор const) означает, что вы фактически создаете другую сигнатуру функции.derived::name является не переопределением base::name и, следовательно, вызов name через ссылку на base не будет отправлен на derived::name !!!

1 голос
/ 14 апреля 2011
using A::PreCompose;
using A::PostCompose;
makes the errors and warnings vanish, but why is this necessary?

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

Вот почему вы должны показать их, просто написав:

using A::PreCompose;
using A::PostCompose;

Другой способ показать их (в данном конкретном случае) - переопределить виртуальные функции из базового класса, которые вы сделали в коде, который вы разместили. Я считаю, что код будет хорошо скомпилироваться.

0 голосов
/ 14 апреля 2011

Классы являются областями видимости, и поиск в базовом классе описывается как поиск в охватывающей области.

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

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

...