Я ищу способ проверить, переопределяет ли дочерний класс функцию базового класса. Сравнение указателей функций-членов отлично работает, если они не виртуальные, но если они являются виртуальные, это не работает. Этот пример кода - это, по сути, то, с чем у меня возникли проблемы.
class Base {
public:
virtual void vfoo(){ cout << "foo"; }
virtual void vbar(){ cout << "bar"; }
void foo(){ cout << "foo"; }
void bar(){ cout << "bar"; }
};
class Child : public Base {
public:
void vfoo(){ cout << "foo2"; }
void foo(){ cout << "foo2"; }
};
int main (){
//non-virtual cases, these work correctly
cout << (&Base::foo == &Child::foo) << endl; //outputs false (good)
cout << (&Base::bar == &Child::bar) << endl; //outputs true (good)
//virtual cases, these do not work correctly
cout << (&Base::vfoo == &Child::vfoo) << endl; //outputs true (BAD, child::vfoo and base::vfoo are DIFFERENT FUNCTIONS)
cout << (&Base::vbar == &Child::vbar) << endl; //outputs true (good, child::vbar and base::vbar are the same)
return 0;
}
Логически нет причин, по которым это не должно работать, но спецификация C++ говорит об обратном (сравнение адресов виртуальных функций таким образом определяется реализацией).
В GCC введите каламбур &Base::vfoo и &Child::vfoo для int, чтобы они оба были равны "1" (а vbar - "9"), которые выглядят как смещения vtable. Следующий код правильно извлекает адреса функций из таблицы vtable и правильно сообщает о разных адресах для Child::vfoo и Base::bfoo и об одном и том же адресе для vbar.
template<typename A, typename B>
A force_cast(B in){
union {
A a;
B b;
} u;
u.b = in;
return u.a;
};
template<typename T>
size_t get_vtable_function_address_o(T* obj, int vtable_offset){
return *((size_t*)((*(char**)obj + vtable_offset-1)));
};
template<typename T, typename F>
size_t get_vtable_function_address(T* obj, F function){
return get_vtable_function_address_o(obj, force_cast<size_t>(function));
};
int main (){
Base* a = new Base();
Base* b = new Child();
cout << get_vtable_function_address(a, &Base::vfoo) << endl;
cout << get_vtable_function_address(b, &Base::vfoo) << endl;
cout << get_vtable_function_address(a, &Base::vbar) << endl;
cout << get_vtable_function_address(b, &Base::vbar) << endl;
return 0;
}
Это отлично работает в GCC, хотя тот факт, что мне нужно вычесть 1 из смещения vtable, чтобы это работало, кажется немного странным. Но это не работает на компиляторе Microsoft (каламбур &Base::vfoo для size_t возвращает какой-то мусор вместо смещения виртуальной таблицы) (некоторые эксперименты здесь предполагают, что правильные смещения здесь будут 0 для vfoo и 4 для vbar)
Я ХОРОШО ЗНАЮ, что этот материал определяется реализацией, но я надеюсь, что есть способ сделать это, который, по крайней мере, работает на нескольких распространенных компиляторах (gcc, msvc и clang), поскольку на данный момент vtables довольно стандартны (даже если для этого требуется код, специфичный для компилятора)?
Есть какой-либо способ сделать это?
Примечание 1. Мне нужно это только для работы с одиночным наследованием. Я не использую множественное или виртуальное наследование
Примечание 2: Еще раз подчеркнув, что мне не нужна вызываемая функция, мне нужно только проверить, перезаписал ли дочерний класс конкретную виртуальную функцию. Если есть способ сделать это без необходимости копаться в виртуальных таблицах для чего-то, то это было бы предпочтительнее.
Читать codeproject.com/Articles/7150/…. Это старо, но проливает некоторый свет на этот вопрос.
Ключевым преимуществом виртуальных функций является то, что ваш код не знает, была ли функция переопределена. Попытка бороться с этим кажется мне недостатком дизайна в начале процесса. (проблема XY?)
@JaMiT У меня есть список объектов определенного базового класса. Не все из них реализуют каждую виртуальную функцию, поэтому для оптимизации я мог бы, например, хранить отдельный список для каждой функции, поэтому, если я хочу вызвать «обновление» для всех объектов, мне нужно будет перебирать только те, которые на самом деле реализовано «обновление» вместо всего списка объектов (который будет содержать множество объектов, где эта функция является просто заполнителем)
Выполнение пустого заполнителя не требует много времени, не так ли? Вы проверили и подтвердили, что эта оптимизация действительно измеримо эффективна? Это может быть много работы без заметной пользы. (Хм... может быть, если бы заполнитель действительно что-то делал - например, возвращал отличительное значение - вы могли бы определить, когда он был вызван без особого взлома. Может быть, аспект заполнителя должен включить это в вопрос?)
это имеет большое значение, если есть 100000 объектов
В C++11 и более поздних версиях, сравнивая типы функций по decltype
и std::is_same
, мы можем получить желаемые результаты.
(Если C++11 недоступен для вас, вы все равно можете использовать для этой цели typeid
и operator==(const type_info& rhs)
.)
Поскольку Base::vfoo
переопределяется Child
, тип decltype(&Child::vfoo)
равен void (Child::*)()
и отличается от decltype(&Base::vfoo)
, который является void (Base::*)()
.
Таким образом
std::is_same<decltype(&Base::vfoo) , decltype(&Child::vfoo)>::value
это false
.
(На самом деле, в пункте 4 черновика стандарта C++ n3337, в котором перечисляется набор неявных преобразований, 4.11 Преобразование указателя на элемент [conv.mem] / 2,
- A prvalue of type “pointer to member of B of type cv T”, where B is a class type, can be converted to a prvalue of type “pointer to member of D of type cv T”, where D is a derived class (Clause 10) of B. If B is an inaccessible (Clause 11), ambiguous (10.2), or virtual (10.1) base class of D, or a base class of a virtual base class of D, a program that necessitates this conversion is ill-formed. The result of the conversion refers to the same member as the pointer to member before the conversion took place, but it refers to the base class member as if it were a member of the derived class. The result refers to the member in D’s instance of B. Since the result has type “pointer to member of D of type cv T”, it can be dereferenced with a D object. The result is the same as if the pointer to member of B were dereferenced with the B subobject of D. The null member pointer value is converted to the null member pointer value of the destination type.
, утверждает, что неявное преобразование из decltype(&Base::vfoo)
в decltype(&Child::vfoo)
может быть допустимым, но не упоминает об обратном направлении.
Кроме того, 5.2.9 Статическое приведение [expr.static.cast] / 12,
- A prvalue of type “pointer to member of D of type cv1 T” can be converted to a prvalue of type “pointer to member of B” of type cv2 T, where B is a base class (Clause 10) of D, if a valid standard conversion from “pointer to member of B of type T” to “pointer to member of D of type T” exists (4.11), and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. Then null member pointer value(4.11) is converted to the null member pointer value of the destination type. If class B contains the original member, or is a base or derived class of the class containing the original member, the resulting pointer to member points to the original member. Otherwise, the result of the cast is undefined. [Note: although class B need not contain the original member, the dynamic type of the object on which the pointer to member is dereferenced must contain the original member; see 5.5. — end note ]
, утверждает, что явное преобразование с использованием static_cast
из decltype(&Child::vfoo)
в decltype(&Base::vfoo)
также может быть допустимым.
Тогда юридические броски друг с другом в этом случае
void (Child::*pb)() = &Base::vfoo;
void (Base ::*pc)() = static_cast<void(Base::*)()>(&Child::vfoo);
и это static_cast
означает, что типы &Base::vfoo
и &Child::vfoo
отличаются друг от друга без какого-либо явного приведения.)
OTOH, поскольку Base::vbar
не переопределяется Child
, тип decltype(&Child::vbar)
— void (Base::*)()
, и то же самое с decltype(&Base::vbar)
.
Таким образом
std::is_same<decltype(&Base::vbar) , decltype(&Child::vbar)>::value
это true
.
(Кажется, что 5.3.1 Унарные операторы [expr.unary.op] / 3 из n3337,
The result of the unary & operator is a pointer to its operand. The operand shall be an lvalue or a qualified- id. If the operand is a qualified-id naming a non-static member m of some class C with type T, the result has type “pointer to member of class C of type T” and is a prvalue designating C::m. Otherwise, if the type of the expression is T, the result has type “pointer to T” and is a prvalue that is the address of the designated object (1.7) or a pointer to the designated function. [ Note: In particular, the address of an object of type “cv T” is “pointer to cv T”, with the same cv-qualification. — end note ] [ Example:
struct A { int i; }; struct B : A { }; ... &B::i ... // has type int A::*
— end example ]
, заявляет об этом поведении. Интересное обсуждение этого абзаца также найдено здесь.)
Таким образом, мы можем проверить, переопределена ли каждая функция-член, используя decltype(&Base::...)
, decltype(&Child::...)
и std::is_same
, следующим образом:
Живая ДЕМО (GCC/Clang/ICC/VS2017)
// Won't fire.
static_assert(!std::is_same<decltype(&Base::foo) , decltype(&Child::foo)> ::value, "oops.");
// Won't fire.
static_assert( std::is_same<decltype(&Base::bar) , decltype(&Child::bar)> ::value, "oops.");
// Won't fire.
static_assert(!std::is_same<decltype(&Base::vfoo), decltype(&Child::vfoo)>::value, "oops.");
// Won't fire.
static_assert( std::is_same<decltype(&Base::vbar), decltype(&Child::vbar)>::value, "oops.");
Кстати, мы также можем определить следующий макрос, чтобы сделать это проще:
#define IS_OVERRIDDEN(Base, Child, Func) \
(std::is_base_of<Base, Child>::value \
&& !std::is_same<decltype(&Base::Func), decltype(&Child::Func)>::value)
что тогда давайте напишем
static_assert( IS_OVERRIDDEN(Base, Child, foo) , "oops."); // Won't fire.
static_assert(!IS_OVERRIDDEN(Base, Child, bar) , "oops."); // Won't fire.
static_assert( IS_OVERRIDDEN(Base, Child, vfoo), "oops."); // Won't fire.
static_assert(!IS_OVERRIDDEN(Base, Child, vbar), "oops."); // Won't fire.
Попробуйте dynamic_cast с тем, с чем вы хотите его сравнить, и если это не удастся, вы знаете, что он его не использует.