Требование переопределенных виртуальных функций для вызова базовых реализаций
В рамках иерархии классов С++ можно ли требовать, чтобы конкретная виртуальная функция всегда вызывала также реализацию базового класса? (Как путь цепи конструкторов?)
Я рассматриваю случай, когда иерархия глубоких классов имеет некоторые общие функции интерфейса, которые каждый ребенок переопределяет. Я бы хотел, чтобы каждый производный класс переопределял цепочку к базовому классу. Прямо сделать это явно, например, с помощью кода ниже, но существует риск того, что кто-то, внедряющий новый производный класс, может забыть пробиться к базе.
Есть ли какой-нибудь шаблон для принудительного применения этого параметра, так что компилятор будет вызывать ошибку, если переопределение не может привязать базу?
Итак, в
class CAA
{
virtual void OnEvent( CEvent *e ) {
// do base implementation stuff;
}
}
class CBB : public CAA
{
typedef CAA BaseClass;
virtual void OnEvent( CEvent *e ) {
DoCustomCBBStuff();
BaseClass::OnEvent( e ); // chain to base
}
}
class CCC : public CBB
{
typedef CBB BaseClass;
virtual void OnEvent( CEvent *e ) {
Frobble();
Glorp();
BaseClass::OnEvent( e ); // chain to CBB which chains to CAA, etc
}
}
class CDD : public CCC
{
typedef CCC BaseClass;
virtual void OnEvent( CEvent *e ) {
Meep();
// oops! forgot to chain to base!
}
}
Есть ли способ, какой-то шаблонный трюк или синтаксический трюк, чтобы сделать CDD более очевидной ошибкой?
Ответы
Ответ 1
То, как это делается, метод базового класса не является виртуальным и вызывает защищенный виртуальный метод.
Конечно, это только один уровень.
В вашей конкретной ситуации большая часть инфраструктуры может заставить ее работать, но она не стоит.
Типичным ответом является добавление комментария
// Always call base class method
Ответ 2
Поместите специальный "скрытый" тип в базовый класс с помощью частного конструктора, используя friend
, чтобы обеспечить его создание только базой. Этот код на ideone.
Если существует несколько уровней, это, к сожалению, не гарантирует, что вызывается немедленный базовый класс. Следовательно, struct E : public D;
может реализовать E::foo()
с вызовом B:: foo
, если вы предпочитаете вызов D::foo()
.
struct Base {
struct Hidden {
friend class Base;
private:
Hidden() {}
};
virtual Hidden foo() {
cout << "Base" << endl;
return Hidden(); // this can create a Hidden
}
};
struct D : public B {
virtual Hidden foo() {
cout << "D" << endl;
// return Hidden(); // error as the constructor is private from here
return B :: foo();
}
};
Если вы попытались реализовать D:: foo() без возврата или с помощью return Hidden()
, вы получите сообщение об ошибке. Единственный способ скомпилировать это - использовать return B :: foo()
.
Ответ 3
Следуя простому правилу для вывода через класс шаблона, возможно.
#include <iostream>
struct TEvent
{
};
struct Base {
virtual void CallOnEvent(TEvent * e)
{
OnEvent(e);
}
virtual void OnEvent(TEvent * e)
{
std::cout << "Base::Event" << std::endl;
}
void CallUp(TEvent * e)
{
}
};
template <typename B>
struct TDerived : public B
{
void CallUp( TEvent * e )
{
B::CallUp(e);
B::OnEvent(e);
}
virtual void CallOnEvent( TEvent * e )
{
CallUp(e);
this->OnEvent(e);
}
};
struct Derived01 : public TDerived< Base >
{
void OnEvent(TEvent * e)
{
std::cout << "Derived01::Event" << std::endl;
}
};
struct Derived02 : public TDerived< Derived01 >
{
void OnEvent(TEvent * e)
{
std::cout << "Derived02::Event" << std::endl;
}
};
struct Derived03 : public TDerived< Derived02 >
{
void OnEvent(TEvent * e)
{
std::cout << "Derived03::Event" << std::endl;
}
};
struct Derived04 : public TDerived< Derived03 >
{
void OnEvent(TEvent * e)
{
std::cout << "Derived04::Event" << std::endl;
}
};
int main( void )
{
Derived04 lD4;
lD4.CallOnEvent(0);
return 0;
}
Этот код дает (codepad):
Base::Event
Derived01::Event
Derived02::Event
Derived03::Event
Derived04::Event
Относительно некоторых ответов, используя typeid
. Я бы никогда не подумал использовать typeid
для чего угодно, кроме отладки. Это связано с двумя вещами:
- проверка динамического типа может выполняться гораздо более эффективными способами (без создания объекта
type_info
, используя dynamic_cast
, некоторые методы
- Стандарт С++ в основном гарантирует только существование типа, но на самом деле не имеет ничего общего с тем, как он работает (большинство вещей "специфичны для компилятора" ).
изменить
Несколько более сложный пример с множественным наследованием.
Этот, к сожалению, не разрешим без явных вызовов в классах, которые наследуются от множества оснований (главным образом потому, что не ясно, что должно произойти в таких случаях, поэтому мы должны явно определять поведение).
#include <iostream>
struct TEvent
{
};
struct Base {
virtual void CallOnEvent(TEvent * e)
{
OnEvent(e);
}
virtual void OnEvent(TEvent * e)
{
std::cout << "Base::Event" << std::endl;
}
void CallUp(TEvent * e)
{
}
};
template <typename B >
struct TDerived : public B
{
void CallUp( TEvent * e )
{
B::CallUp(e);
B::OnEvent(e);
}
virtual void CallOnEvent( TEvent * e )
{
CallUp(e);
this->OnEvent(e);
}
};
struct Derived01 : virtual public TDerived< Base >
{
void OnEvent(TEvent * e)
{
std::cout << "Derived01::Event" << std::endl;
}
};
struct Derived02 : virtual public TDerived< Derived01 >
{
void OnEvent(TEvent * e)
{
std::cout << "Derived02::Event" << std::endl;
}
};
typedef TDerived< Derived02 > TDerived02;
typedef TDerived< Derived01 > TDerived01;
struct Derived03 : virtual public TDerived02, virtual public TDerived01
{
void OnEvent(TEvent * e)
{
std::cout << "Derived03::Event" << std::endl;
}
virtual void CallOnEvent( TEvent * e )
{
CallUp(e);
Derived03::OnEvent(e);
}
void CallUp( TEvent * e )
{
TDerived02::CallUp(e);
TDerived01::CallUp(e);
}
};
struct Derived04 : public TDerived< Derived03 >
{
void OnEvent(TEvent * e)
{
std::cout << "Derived04::Event" << std::endl;
}
};
int main( void )
{
Derived04 lD4;
Derived03 lD3;
lD3.CallOnEvent( 0 );
std::cout << std::endl;
lD4.CallOnEvent( 0 );
return ( 0 );
}
Результат (ideone):
Base::Event \ \
Derived01::Event | - from Derived02 |
Derived02::Event / |-- from Derived03
Base::Event \__ from Derived01 |
Derived01::Event / |
Derived03::Event /
Base::Event \ \ \
Derived01::Event | - from Derived02 | |
Derived02::Event / |-- from Derived03 |-- from Derived04
Base::Event \__ from Derived01 | |
Derived01::Event / | |
Derived03::Event / |
Derived04::Event /
Ответ 4
Нет поддержки для этого на языке С++, но, добавив комментарий KerrekSB, вы можете сделать что-то вроде этого:
class A {
public:
void DoEvent(int i) {
for (auto event = events.begin(); event != events.end(); ++event)
(this->*(*event))(i);
}
protected:
typedef void (A::*Event)(int);
A(Event e) {
events.push_back(&A::OnEvent);
events.push_back(e);
}
void OnEvent(int i) {
cout << "A::OnEvent " << i << endl;
}
vector<Event> events;
};
class B : public A {
public:
B() : A((Event)&B::OnEvent) { }
protected:
B(Event e) : A((Event)&B::OnEvent) {
events.push_back(e);
}
void OnEvent(int i) {
cout << "B::OnEvent " << i << endl;
}
};
class C : public B {
public:
C() : B((Event)&C::OnEvent) { }
protected:
C(Event e) : B((Event)&C::OnEvent) {
events.push_back(e);
}
void OnEvent(int i) {
cout << "C::OnEvent " << i << endl;
}
};
Затем используйте его так:
int main() {
A* ba = new B;
ba->DoEvent(32);
B* bb = new B;
bb->DoEvent(212);
A* ca = new C;
ca->DoEvent(44212);
B* cb = new C;
cb->DoEvent(2);
C* cc = new C;
cc->DoEvent(9);
}
Это выводит
A::OnEvent 32
B::OnEvent 32
A::OnEvent 212
B::OnEvent 212
A::OnEvent 44212
B::OnEvent 44212
C::OnEvent 44212
A::OnEvent 2
B::OnEvent 2
C::OnEvent 2
A::OnEvent 9
B::OnEvent 9
C::OnEvent 9
Вам нужно немного поработать, но вам не нужно вручную вызывать функцию члена baser в конце каждого вызова. Вот живая демонстрация.
Ответ 5
Нет ничего прямого принуждения функции переопределения, чтобы делать что-либо в частности, за исключением возврата определенного типа. Однако, если вы создаете виртуальную функцию базового класса private
, никакая функция не может вызвать ее в базовом классе, но производные классы могут ее переопределить. Затем вы также предоставляете функцию public
, которая вызывает виртуальную функцию, а также функцию, выполняющую логику базового класса. Логика из базового класса, вероятно, должна перейти в отдельную функцию (возможно, не виртуальную функцию пересылки), чтобы избежать ее выполнения дважды, если объект фактически является базовым объектом.