Двойная отправка выдает предупреждения "скрывает виртуальную функцию", почему?

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

#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, спасибо. Как насчет моего первого вопроса? Любые комментарии по этому подходу?

3 ответа

Решение

Двойная диспетчеризация обычно реализуется по-разному в 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и lookup проверит все функции-члены в derived, идентификатор foo находится там, и поиск не идет вверх. Аргументом единственной доступной перегрузки является std::string const&и компилятор добавит неявное преобразование, поэтому, даже если может быть наилучшее потенциальное совпадение (base::foo(const char*) это лучший матч, чем derived::foo(std::string const&) для этого вызова) он будет эффективно вызывать:

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, Он находит две перегрузки, и одна из них хорошо подходит, поэтому он вызовет 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" ????
}

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

using A::PreCompose;
using A::PostCompose;
makes the errors and warnings vanish, but why is this necessary?

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

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

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

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

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

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

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

Другие вопросы по тегам