Наследование C++ и указатели на функции-члены
-
09-06-2019 - |
Вопрос
Можно ли в C++ использовать указатели на функции-члены для указания на члены производного (или даже базового) класса?
РЕДАКТИРОВАТЬ:Возможно, пример поможет.Предположим, у нас есть иерархия из трех классов. X
, Y
, Z
в порядке наследования.Y
поэтому имеет базовый класс X
и производный класс Z
.
Теперь мы можем определить указатель на функцию-член. p
для класса Y
.Это написано как:
void (Y::*p)();
(Для простоты я предполагаю, что нас интересуют только функции с сигнатурой void f()
)
Этот указатель p
теперь можно использовать для указания на функции-члены класса Y
.
Тогда этот вопрос (на самом деле два вопроса):
- Может
p
использоваться для указания на функцию в производном классеZ
? - Может
p
использоваться для указания на функцию в базовом классеX
?
Решение
С++03 стандартный, §4.11 2 Указатель на преобразования членов:
Значение типа «указатель на член B типа резюме T», где B — тип класса, можно преобразовать в значение r типа «указатель на член D типа резюме T», где D — производный класс (пункт 10) от B.Если B является недоступным (пункт 11), неоднозначным (10.2) или виртуальным (10.1) базовым классом D, программа, требующая этого преобразования, является неправильной.Результат преобразования относится к тому же члену, что и указатель на член до того, как произошло преобразование, но он ссылается на член базового класса, как если бы он был членом производного класса.Результат относится к члену в экземпляре B, принадлежащем D.Поскольку результат имеет тип «указатель на член D типа резюме 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» можно преобразовать в значение rvalue типа «указатель на член B типа cv2 T", где B — базовый класс (пункт 10 класс.производное) из D, если существует допустимое стандартное преобразование из «указателя на член B типа T» в «указатель на член D типа T» (4.11 конв.память), и cv2 является той же резюме-квалификацией, что и резюме, или выше, чем, cv1.11) Значение указателя нулевого элемента (4.11 конв.память) преобразуется в значение нулевого указателя элемента целевого типа.Если класс B содержит исходный член или является базовым или производным классом класса, содержащего исходный член, результирующий указатель на член указывает на исходный член.В противном случае результат приведения не определен.[Примечание:хотя класс B не обязательно должен содержать исходный член, динамический тип объекта, на котором разыменовывается указатель на член, должен содержать исходный член;видеть 5.5 выраж.mptr.oper.]
11) Типы функций (включая те, которые используются в указателе на типы функций-членов) никогда не имеют CV-квалификации;видеть 8.3.5 dcl.fct.
Короче говоря, вы можете конвертировать из производного D::*
на базу B::*
если вы можете конвертировать из B::*
к D::*
, хотя вы можете использовать только B::*
на объектах типа D или потомках D.
Другие советы
Я не уверен на 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;
}
Критическая проблема с указателями на члены заключается в том, что их можно применять к любой ссылке или указателю на класс правильного типа.Это означает, что поскольку 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
Возможно, вы захотите ознакомиться с этой статьей Указатели на функции-члены и самые быстрые делегаты C++ Короткий ответ в некоторых случаях – да.
Я так считаю.Поскольку указатель функции использует подпись для идентификации себя, базовое/производное поведение будет зависеть от любого объекта, к которому вы его вызвали.
Мои эксперименты показали следующее:Внимание: это может быть неопределенное поведение.Было бы полезно, если бы кто-нибудь мог дать точную ссылку.
- Это сработало, но потребовало приведения типов при назначении производной функции-члена
p
. - Это тоже сработало, но потребовало дополнительных приведения при разыменовании.
p
.
Если мы чувствуем себя действительно амбициозными, мы могли бы спросить, p
может использоваться для указания на функции-члены несвязанных классов.Я не пробовал, но Фастделегат Страница, связанная с ответом Дагорима, предполагает, что это возможно.
В заключение я постараюсь избегать использования указателей на функции-члены таким образом.Подобные отрывки не внушают доверия:
Кастинг между указателями функций участников является чрезвычайно темной областью.Во время стандартизации C ++ было много обсуждений о том, должны ли вы иметь возможность разыграть указатель функции участника из одного класса в указатель функции члена базового или полученного класса, и можете ли вы разыграть между не связанными классами.К тому времени, когда комитет по стандартам решил, что разные поставщики компилятора уже приняли решения о реализации, которые зафиксировали их в разных ответах на эти вопросы.[Статья FastDelegate]
Предположим, что мы имеем 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, что может привести к неизвестно чему.Вы можете заставить все это работать, приводя указатели, но это небезопасно и не гарантирует работу.
Вот пример того, что работает.Вы можете переопределить метод в производном классе, и другой метод базового класса, который использует указатель на этот переопределенный метод, действительно вызывает метод производного класса.
#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;
}