C ++ наследование и указатели на функции-члены - PullRequest
43 голосов
/ 13 сентября 2008

В C ++ можно ли использовать указатели на функции-члены для указания на производные (или даже базовые) члены класса?

EDIT: Возможно, пример поможет. Предположим, у нас есть иерархия из трех классов X, Y, Z в порядке наследования. Следовательно, Y имеет базовый класс X и производный класс Z.

Теперь мы можем определить указатель на функцию-член p для класса Y. Это написано как:

void (Y::*p)();

(Для простоты я предполагаю, что нас интересуют только функции с подписью void f())

Этот указатель p теперь можно использовать для указания на функции-члены класса Y.

Этот вопрос (правда, два вопроса):

  1. Может ли p использоваться для указания на функцию в производном классе Z?
  2. Может ли p использоваться для указания на функцию в базовом классе X?

Ответы [ 8 ]

28 голосов
/ 22 апреля 2010

C ++ 03 std, §4.11 2 Указатель на преобразование члена :

Значение типа «указатель на член B типа cv T», где B - тип класса, может быть преобразовано в значение типа «указатель на член D типа * 1008». * cv T, ”где D - это производный класс (пункт 10) класса B. Если B - недоступный (пункт 11), неоднозначный (10.2) или виртуальный (10.1) базовый класс D, программа, для которой это необходимо обращение плохо сформировано. Результат преобразования ссылается на тот же элемент, что и указатель на член до преобразования, но ссылается на элемент базового класса, как если бы он был членом производного класса. Результат ссылается на член в экземпляре D из B. Поскольку результат имеет тип «указатель на член D типа cv T», его можно разыменовать с помощью объекта D. Результат такой же, как если бы указатель на член B был разыменован с подобъектом B объекта D. Значение указателя с нулевым элементом преобразуется в значение указателя с нулевым элементом типа назначения. 52)

52) Правило для преобразования указателей на элементы (от указателя на элемент базы к указателю на элемент производного) выглядит инвертированным по сравнению с правилом для указателей на объекты (от указателя на производный к указателю) на базу) (4.10, пункт 10). Эта инверсия необходима для обеспечения безопасности типов. Обратите внимание, что указатель на член не является указателем на объект или указателем на функцию, и правила преобразования таких указателей не применяются к указателям на члены. В частности, указатель на член не может быть преобразован в void *.

Короче говоря, вы можете преобразовать указатель на член доступного, не виртуального базового класса в указатель на член производного класса, если этот член не является неоднозначным.

class A {
public: 
    void foo();
};
class B : public A {};
class C {
public:
    void bar();
};
class D {
public:
    void baz();
};
class E : public A, public B, private C, public virtual D {
public: 
    typedef void (E::*member)();
};
class F:public E {
public:
    void bam();
};
...
int main() {
   E::member mbr;
   mbr = &A::foo; // invalid: ambiguous; E's A or B's A?
   mbr = &C::bar; // invalid: C is private 
   mbr = &D::baz; // invalid: D is virtual
   mbr = &F::bam; // invalid: conversion isn't defined by the standard
   ...

Преобразование в другом направлении (через static_cast) регулируется § 5.2.9 9:

Значение типа "указатель на член D типа cv1 T" может быть преобразовано в значение типа "указатель на член B типа cv2 T", где B - базовый класс (пункт 10 class.derived ) D, если существует допустимое стандартное преобразование из «указателя на член B типа T» в «указатель на член D типа T» ( 4.11 conv.mem ), и cv2 - это то же самое cv-квалификация, что и квалификация cv или более высокая, чем cv1 . 11) 1043 * Значение указателя нулевого элемента ( 4.11 conv.mem ) преобразуется в значение указателя нулевого элемента целевого типа. Если класс B содержит исходный член или является базовым или производным классом класса, содержащего исходный член, результирующий указатель на член указывает на исходный член. В противном случае результат приведения не определен. [Примечание: хотя класс B не должен содержать исходный член, динамический тип объекта, на который ссылается указатель на член, должен содержать исходный член; см. 5.5 expr.mptr.oper .]

11) Типы функций (включая те, которые используются в указателе на функцию-член типы) никогда не квалифицируются как cv; см 8.3.5 dcl.fct .

Короче говоря, вы можете конвертировать из производного D::* в базовое B::*, если вы можете конвертировать из B::* в D::*, хотя вы можете использовать B::* только для объектов, которые имеют тип D или происходят от D.

11 голосов
/ 13 сентября 2008

Я не на 100% уверен, что вы спрашиваете, но вот пример, который работает с виртуальными функциями:

#include <iostream>
using namespace std;

class A { 
public:
    virtual void foo() { cout << "A::foo\n"; }
};
class B : public A {
public:
    virtual void foo() { cout << "B::foo\n"; }
};

int main()
{
    void (A::*bar)() = &A::foo;
    (A().*bar)();
    (B().*bar)();
    return 0;
}
7 голосов
/ 22 апреля 2010

Критическая проблема с указателями на члены заключается в том, что они могут применяться к любой ссылке или указателю на класс правильного типа. Это означает, что поскольку Z получено из Y, указатель (или ссылка) указателя типа (или ссылки) на Y может фактически указывать (или ссылаться) на подобъект базового класса Z или любой другой класс производный от Y.

void (Y::*p)() = &Z::z_fn; // illegal

Это означает, что все, что назначено указателю на член Y, должно фактически работать с любым Y. Если бы было разрешено указывать на член Z (который не был членом Y), то можно было бы вызвать функцию-член Z для некоторой вещи, которая на самом деле не была Z.

С другой стороны, любой указатель на член Y также указывает на член Z (наследование означает, что Z имеет все атрибуты и методы его базы), если это допустимо для преобразования указателя на член Y на указатель на член Z. Это по сути безопасно.

void (Y::*p)() = &Y::y_fn;
void (Z::*q)() = p; // legal and safe
3 голосов
/ 13 сентября 2008

Возможно, вы захотите ознакомиться с этой статьей Указатели на функции-члены и максимально быстрые делегаты C ++ В некоторых случаях краткий ответ - да.

1 голос
/ 22 апреля 2010

Предположим, что у нас есть class X, class Y : public X, and class Z : public Y

Вы должны иметь возможность назначать методы для обоих X, Y указателям типа void (Y :: * p) (), но не для методов для Z. Чтобы понять, почему следует учитывать следующее:

void (Y::*p)() = &Z::func; // we pretend this is legal
Y * y = new Y; // clearly legal
(y->*p)(); // okay, follows the rules, but what would this mean?

Допуская это присваивание, мы разрешаем вызов метода для Z для объекта Y, что может привести к тому, кто что знает. Вы можете заставить все это работать, используя указатели, но это небезопасно или гарантированно не работает.

1 голос
/ 15 июля 2009

Мои эксперименты показали следующее: Предупреждение - это может быть неопределенное поведение. Было бы полезно, если бы кто-то мог предоставить точную ссылку.

  1. Это сработало, но требовало приведение при назначении производной функции-члена p.
  2. Это также сработало, но требовало дополнительных приведений при разыменовании p.

Если мы чувствуем себя действительно амбициозными, мы можем спросить, можно ли использовать p для указания на функции-члены не связанных классов. Я не пробовал, но страница FastDelegate , на которую ссылается Дагорым, предполагает, что это возможно.

В заключение я постараюсь избежать использования указателей на функции-члены таким образом. Такие отрывки не внушают доверия:

Приведение между функциями-членами указатели - это очень темная область. Во время стандартизации C ++, было много дискуссий о должны ли вы быть в состоянии разыграть указатель на функцию-член из одного класса на указатель на функцию-член базы или производный класс, и вы мог бы бросить между несвязанными классами. К тому времени комитет по стандартам решились, другой компилятор продавцы уже сделали решения о реализации, которые имели заперли их на разные ответы эти вопросы. [ Статья FastDelegate ]

1 голос
/ 13 сентября 2008

Я верю в это. Поскольку указатель на функцию использует сигнатуру для идентификации себя, базовое / производное поведение будет зависеть от любого объекта, к которому вы его вызывали.

0 голосов
/ 13 февраля 2015

Вот пример того, что работает. Вы можете переопределить метод в производном классе, и другой метод базового класса, который использует указатель на этот переопределенный метод, действительно вызывает метод производного класса.

#include <iostream>
#include <string>

using namespace std;

class A {
public:
    virtual void traverse(string arg) {
        find(&A::visit, arg);
    }

protected:
    virtual void find(void (A::*method)(string arg),  string arg) {
        (this->*method)(arg);
    }

    virtual void visit(string arg) {
        cout << "A::visit, arg:" << arg << endl;
    }
};

class B : public A {
protected:
    virtual void visit(string arg) {
        cout << "B::visit, arg:" << arg << endl;
    }
};

int main()
{
    A a;
    B b;
    a.traverse("one");
    b.traverse("two");
    return 0;
}
...