С ++ наследование и указатели на функции-члены

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

РЕДАКТИРОВАТЬ: Возможно, поможет пример. Предположим, у нас есть иерархия из трех классов X, Y, Z в порядке наследования. Y поэтому имеет базовый класс X и производный класс Z.

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

void (Y::*p)();

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

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

Этот вопрос (на самом деле два вопроса) таков:

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

person smh    schedule 12.09.2008    source источник
comment
Зачем тебе это нужно?   -  person Landon    schedule 13.09.2008
comment
Я согласен с Мэттом. Ваш вопрос требует пояснения. Вы просто хотите знать, работают ли указатели на функции-члены с виртуальными функциями? Если да, то да. Если вы пытаетесь спросить что-то еще, поясните, пожалуйста.   -  person Derek Park    schedule 13.09.2008
comment
Думаю, ясно, как и просили. +1 и smh: Вам нужны дополнительные объяснения? Укажите, пожалуйста, так ли это.   -  person Johannes Schaub - litb    schedule 26.10.2009


Ответы (8)


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

Rvalue типа «указатель на член B типа cv T», где B - тип класса, может быть преобразовано в rvalue типа «указатель на член D типа 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:

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

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

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

person outis    schedule 22.04.2010
comment
+1. На странице stackoverflow.com/questions/4295117/ они находят что 5.2.9 / 9 говорит, что static_cast может преобразовать T D::* в T B::*, хотя, конечно, для этого требуется, чтобы вы либо (а) знали, что нельзя вызывать через него несуществующие методы; или (б) знать, что динамический тип объекта B на самом деле D или что-то производное от него. - person j_random_hacker; 28.11.2010
comment
Короче говоря, вы можете преобразовать производный D::* в базовый B::*, если можете преобразовать из B::* в D::*, хотя вы можете использовать B::* только для объектов типа D или производных от D. - ложный. Если член, на который указывает D::*, не содержится в B (или его базовых классах), то уже приведение указателя к B::* приводит к неопределенному поведению. - person j_kubik; 09.07.2013
comment
@j_kubik: не по стандарту; как указано в цитате, если класс B [...] является базовым [...] классом класса, содержащего исходный член, результирующий указатель на член указывает на исходный член. Это меня тоже удивило, когда я впервые его прочитал. Также: Примечание: хотя класс B не обязательно должен содержать исходный член, динамический тип объекта, на котором разыменовывается указатель на член, должен содержать исходный член. Моя последняя фраза все еще в силе. - person outis; 10.07.2013
comment
Хорошо, а что, если у B нет виртуальных методов, а у D есть? Тогда указатель на метод B для некоторых компиляторов будет иметь другой размер (обычно меньше), чем указатель на D. Поскольку эти дополнительные байты добавляются с какой-то целью (почему?), Сокращенный указатель на B::* не сможет правильно представить метод D, даже если динамический тип объекта - D. - person j_kubik; 10.07.2013
comment
Хорошо, теперь я заметил, что такое приведение (с некоторыми ограничениями, вводимыми стандартом) на самом деле может быть выполнено без добавления чего-либо в существующие реализации. В любом случае, это довольно удивительно и, на мой взгляд, весьма полезно. - person j_kubik; 10.07.2013
comment
@j_kubik: а-хюп. Я просто полагаю, что комитет по стандартам знает больше, чем мы, и имел свои причины. Поскольку я не реализую компилятор, это легко принять. Кто знает? Если бы я был, возможно, я бы больше жаловался. - person outis; 10.07.2013
comment
Я также не пишу компилятор, но знание одного или двух примеров реализации языковых конструкций C / C ++ в любом случае может быть хорошей идеей. - person j_kubik; 11.07.2013

Я не уверен на 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;
}
person Matt Price    schedule 12.09.2008

Критическая проблема с указателями на члены заключается в том, что они могут применяться к любой ссылке или указателю на класс правильного типа. Это означает, что поскольку 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
person CB Bailey    schedule 22.04.2010

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

person dagorym    schedule 12.09.2008

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

person Steve Duitsman    schedule 12.09.2008

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

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

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

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

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

person smh    schedule 15.07.2009

Предположим, что у нас есть 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, который может привести к неизвестно чему. Вы можете заставить все это работать, применяя указатели, но это небезопасно или гарантированно сработает.

person Winston Ewert    schedule 22.04.2010
comment
Вы имеете в виду (y->*p)(); в последней строке? - person j_random_hacker; 28.11.2010
comment
@j_random_hacker, наверное. Я провожу слишком много времени за языками, синтаксис которых нормальный. - person Winston Ewert; 28.11.2010

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

#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;
}
person Gena Batsyan    schedule 13.02.2015