Проверить, переопределяет ли подкласс виртуальную функцию в базовом классе

Я ищу способ проверить, переопределяет ли дочерний класс функцию базового класса. Сравнение указателей функций-членов отлично работает, если они не виртуальные, но если они являются виртуальные, это не работает. Этот пример кода - это, по сути, то, с чем у меня возникли проблемы.

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: Еще раз подчеркнув, что мне не нужна вызываемая функция, мне нужно только проверить, перезаписал ли дочерний класс конкретную виртуальную функцию. Если есть способ сделать это без необходимости копаться в виртуальных таблицах для чего-то, то это было бы предпочтительнее.

Попробуйте dynamic_cast с тем, с чем вы хотите его сравнить, и если это не удастся, вы знаете, что он его не использует.

solarflare 08.04.2019 00:43

Читать codeproject.com/Articles/7150/…. Это старо, но проливает некоторый свет на этот вопрос.

Passer By 08.04.2019 01:02

Ключевым преимуществом виртуальных функций является то, что ваш код не знает, была ли функция переопределена. Попытка бороться с этим кажется мне недостатком дизайна в начале процесса. (проблема XY?)

JaMiT 08.04.2019 02:03

@JaMiT У меня есть список объектов определенного базового класса. Не все из них реализуют каждую виртуальную функцию, поэтому для оптимизации я мог бы, например, хранить отдельный список для каждой функции, поэтому, если я хочу вызвать «обновление» для всех объектов, мне нужно будет перебирать только те, которые на самом деле реализовано «обновление» вместо всего списка объектов (который будет содержать множество объектов, где эта функция является просто заполнителем)

TylerGlaiel 08.04.2019 02:16

Выполнение пустого заполнителя не требует много времени, не так ли? Вы проверили и подтвердили, что эта оптимизация действительно измеримо эффективна? Это может быть много работы без заметной пользы. (Хм... может быть, если бы заполнитель действительно что-то делал - например, возвращал отличительное значение - вы могли бы определить, когда он был вызван без особого взлома. Может быть, аспект заполнителя должен включить это в вопрос?)

JaMiT 09.04.2019 06:15

это имеет большое значение, если есть 100000 объектов

TylerGlaiel 10.04.2019 07:21
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
6
1 549
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В 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,

  1. 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,

  1. 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,

  1. 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.

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