Двойная отправка выдает предупреждения "скрывает виртуальную функцию", почему?
Я хотел бы реализовать взаимодействия между двумя объектами, чьи типы являются производными от общего базового класса. Существует взаимодействие по умолчанию, и определенные вещи могут происходить, когда взаимодействуют объекты одного типа. Это реализовано с использованием следующей схемы двойной отправки:
#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"
}
У меня есть два, к сожалению, совершенно разные вопросы, касающиеся этого кода:
- Как вы думаете, это разумный подход? Вы бы предложили что-то другое?
- Если я опущу первые два
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 импортирует определение из прилагаемой области видимости и является нормальным решением.