Указатели функций-членов и наиболее быстрые делегаты C++ - Странности указателей функций-членов

ОГЛАВЛЕНИЕ

 

Странности указателей функций-членов

Указатели функций-членов имеют ряд странностей. Первое, вы не можете использовать указатель члена, чтобы ссылаться на статическую функцию-член. Для этого нужно использовать обычный указатель функции. (Поэтому название "указатель функции-члена" немного сбивает с толку: в реальности они являются "нестатическими указателями функций-членов".) Второе, когда вы имеете дело с производными классами, есть несколько неожиданностей. Например, код ниже будет компилироваться в MSVC, если вы не будете изменять комментарии:

class SomeClass {
 public:
    virtual void some_member_func(int x, char *p) {
       printf("In SomeClass"); };
};

class DerivedClass : public SomeClass {
 public:
 // Если раскомментировать следующую строку, код в строке (*) не будет работать!
//    virtual void some_member_func(int x, char *p) { printf("In DerivedClass"); };
};

int main() {
    // Объявляем указатель функции-члена для SomeClass
    typedef void (SomeClass::*SomeClassMFP)(int, char *);
    SomeClassMFP my_memfunc_ptr;
    my_memfunc_ptr = &DerivedClass::some_member_func; // ---- строка (*)
}

Как ни странно, &DerivedClass::some_member_func является указателем функции-члена класса SomeClass. Он не является членом DerivedClass! (Некоторые компиляторы поступают немного иначе: например, для Digital Mars C++, &DerivedClass::some_member_func в этом случае является неопределенным.) Но если DerivedClass подменяет some_member_func, код не будет компилироваться, так как &DerivedClass::some_member_func теперь стал указателем функции-члена класса DerivedClass!

Преобразование типов между указателями функций-членов – это очень туманная область. Во время стандартизации C++ велись споры о том, должны ли вы иметь возможность приводить указатель функции-члена одного класса к указателю функции-члена базового или производного класса, и можно ли выполнять преобразования типов между несвязанными классами. К тому моменту, когда комитет по стандартам принял решение, различные производители компиляторов уже реализовали решения, которые привязали их к различным ответам на эти вопросы. Согласно стандартам (раздел 5.2.10/9), вы можете использовать reinterpret_cast, чтобы сохранить функцию-член одного класса внутри указателя функции-члена несвязанного класса. Результат вызова преобразованной функции-члена неопределенный. Вы можете только привести его обратно к классу, из которого он произошел. Мы обсудим это позже, так как в этой области стандарты имеют мало сходства с реальными компиляторами.

В некоторых компиляторах странности происходят даже при преобразовании между указателями функций-членов базовых и производных классов. Когда применяется множественное наследование, использование reinterpret_cast для преобразования от исходного класса к базовому классу может компилироваться или не компилироваться, в зависимости от того, в каком порядке классы перечислены в объявлении производного класса! Вот пример:

class Derived: public Base1, public Base2 // случай (а)
class Derived2: public Base2, public Base1 // случай (б)
typedef void (Derived::* Derived_mfp)();
typedef void (Derived2::* Derived2_mfp)();
typedef void (Base1::* Base1mfp) ();
typedef void (Base2::* Base2mfp) ();
Derived_mfp x;

В случае (а) static_cast<Base1mfp>(x) будет работать, но static_cast<Base2mfp>(x) не будет работать. Но для случая (б) верно обратное. Вы можете безопасно преобразовывать указатель функции-члена из производного класса только в его первый базовый класс! Если вы попытаетесь это сделать, MSVC выдаст предупреждение C4407, а Digital Mars C++ выдаст ошибку. Оба будут возражать, если вы используете reinterpret_cast вместо static_cast, но по разным причинам. Но некоторые компиляторы воспримут все это нормально.

В стандартах есть еще одно интересное правило: вы можете объявить указатель функции-члена перед определением класса. Вы даже можете вызвать функцию-член данного незавершенного типа! Это будет обсуждаться позже в статье. Учтите, что некоторые компиляторы не поддерживают эту возможность (ранний MSVC, ранний CodePlay, LVMM).

Не столь важно, что наряду с указателями функций-членов стандарт C++ таже предоставляет указатели членов-данных. Они имеют одинаковые операторы и ряд одинаковых проблем в реализации. Они применялись в нескольких реализациях stl::stable_sort, но мы не знаем, есть ли другие области их использования.