А как правильно сделать?
Суть задачи — есть базовый класс. В ней есть метод в стиле: пробежаться по структуре, найти по имени метод который нужно вызывать и вызвать его. Проблема в том, что методы, которые надо вызывать определяются в наследуемых классах.
R>Так тебе предложили вариант. Ты не заметил сделанных изменений?
Ссори, не заметил.
Но с виртуалом немного не то. Я не знаю сколько и каких методов будет в наследуемых классах. Мне их надо вызывать по текстовой строке.
Например, надо вот такое:
Здравствуйте, PavelCH, Вы писали:
R>>Так тебе предложили вариант. Ты не заметил сделанных изменений? PCH>Ссори, не заметил. PCH>Но с виртуалом немного не то. Я не знаю сколько и каких методов будет в наследуемых классах. Мне их надо вызывать по текстовой строке. PCH>Например, надо вот такое: PCH> . . .
Опасность такого дизайна в том, что он свободно позволяет вызывать функции-члены классов для объеков, не являющихся экземплярами этих классов. Неопределенное поведение и трудно диагностируемые ошибки. Возможно, тебе имеет смысл посмотреть в сторону концепций и статического полиморфизма.
--
Не можешь достичь желаемого — пожелай достигнутого.
R>Опасность такого дизайна в том, что он свободно позволяет вызывать функции-члены классов для объеков, не являющихся экземплярами этих классов. Неопределенное поведение и трудно диагностируемые ошибки. Возможно, тебе имеет смысл посмотреть в сторону концепций и статического полиморфизма.
Тут проблема в том что я не знаю конечного набора функций которые могут быть в наследуемых классах и которые надо будет вызывать. Есть какие-то рекомендации как решать подобные проблемы?
Здравствуйте, PavelCH, Вы писали:
PCH>Тут проблема в том что я не знаю конечного набора функций которые могут быть в наследуемых классах и которые надо будет вызывать. Есть какие-то рекомендации как решать подобные проблемы?
Мне трудно что-то посоветовать. Полагаю, в такой постановке, задача не имеет безопасного решения. Я бы в первую очередь попытался понять, каким образом возникла такая портребность. Весьма вероятно, это результат какой-нибуль другой ошибки дизайна.
--
Не можешь достичь желаемого — пожелай достигнутого.
Здравствуйте, PavelCH, Вы писали:
R>>Так тебе предложили вариант. Ты не заметил сделанных изменений? PCH>Ссори, не заметил. PCH>Но с виртуалом немного не то. Я не знаю сколько и каких методов будет в наследуемых классах. Мне их надо вызывать по текстовой строке. PCH>Например, надо вот такое: PCH>
PCH>struct ca
PCH>{
PCH>cc t1;
PCH>ca* p = &t1;
PCH>(p->*cc::commands[0].proc)();
PCH>
А чем продиктован этот дизайн? Проблема вызова произвольной функции-члена в произвольной точке иерархии наследования решается наивно, но безопасно:
1) Не хочется писать "суппорт" код по поиску метода по строке и вызову его в каждом наследуемом классе. Но на самом деле это мелочь. Больше по п.2
2) Нет возможности получить список методов класса в формализованной однотипной структуре. То есть по условиям моей задачи надо у объекта также иметь список того что он может.
Здравствуйте, PavelCH, Вы писали:
PCH>Но с виртуалом немного не то. Я не знаю сколько и каких методов будет в наследуемых классах. Мне их надо вызывать по текстовой строке.
+1 к вопросу rg45.
В качестве варианта могу предложить хранить не ca::proc, а лямбду, завернутую в std::function<ca*>.
Лямбда для f2 будет такой:
[](ca* base)
{
dynamic_cast<cb&>(*base).f2();
}
Так хотя бы в рантайме будет защита от попытки подсунуть указатель на cc, а не на cb.
Здравствуйте, PavelCH, Вы писали:
PCH>struct ca PCH>{ PCH> int a, b; PCH> ca() : a(0), b(0) PCH> { PCH> } PCH> void func() PCH> { PCH> a++; PCH> } PCH> typedef void(ca::*proc)(); PCH>}; PCH>struct cb : ca PCH>{ PCH> void f2() PCH> { PCH> b++; c++; PCH> } PCH>};
PCH>} pdata[] = { PCH> {"f2", (ca::proc)&cb::f2}, // Тут опасный код PCH> {"func", &ca::func}, PCH>}; PCH>cb t1; PCH>ca* p = &t1; PCH>(p->*pdata[0].proc)(); // Есть ли какие-то подводные камни в таком вызове? PCH>[/ccode]
Методы классов-наследников могут обращаться к данным классов-наследников (логично же),
а ты преобразуешь этот метод к указателю на (нестатический) метод класса-предка.
При вызове метода класса через указатель на метод класса ты обязан в вызове передать
ссылку на объект класса.
Указатель на метод класса-предка может быть вызван с любым объектом, являющимся
объектом класса-предка (ca в данном случае). Это значит, что это могут быть
объекты конечного класса ca, объекты конечного класс cb или любых наследников
(тут не определённых, но потенциально определяемых в будущем) от обоих этих классов,
т.е. как наследников ca, так и наследников cb (которые естественно являются наследниками и ca также).
При этом вызов методов класса cb для объектов разных конечных типов будет либо валидным, либо невалидным,
поскольку метод класса cb будет вызван с объектом, который НЕ является классом cb.
(нарушение принципа подстановки Лисков).
Для разных конечных классов объекта:
-- Класс ca -- вызов невалидный, поскольку реальный тип объекта — ca, а не cb
-- Класс cb -- вызов валидный, поскольку реальный тип объекта — cb
-- Класс -- какой-то наследник ca, но минуя в наследовании cb -- вызов невалидный, поскольку реальный тип объекта не cb
-- Класс -- какой-то наследник cb -- вызов валидный, поскольку реальный тип объекта cb
Соответственно, в части вариантов вызовов нарушается принцип подстановки и программа может быть невалидной.
Именно поэтому приведение указателя на метод или член класса-наследника к указателю на метод или член класса-предка
является неопределённым поведением (не проверял вот прямо сейчас в стандарте, но уверен, что именно так дело и обстоит).
Т.е. так писать программу нельзя.
Однако, если метод класса-наследника, который приведён к указателю на метод класса-предка, не обращается
к нестатическим данным класса-наследника и не вызывает нестатические методы класса-наследника, которые в свою очередь
могут иметь доступ к нестатическим данным класса-наследника, или после преобразования к указателю на
метод класса-предка через этот указатель метод будет вызываться только с объектами, являющимися объектами
класса-наследника, метод наследника может нормально работать.
Здравствуйте, MasterZiv, Вы писали:
MZ>Т.е. так писать программу нельзя.
Реально такое преобразование применяется и работает на практике, например, в MFC, таким образом там
оформляются карты сообщений оконных классов. Методы классов приводятся к указателям на методы
базового класса CCmdTarget.
При этом после преобразования к указателю на метод класса-предка данная функция-член
будет вызываться только с объектами, являющимися объектами этого оконного класса или его классов-наследников.
Это обеспечивается логикой обработки сообщений внутри MFC.
Здравствуйте, MasterZiv, Вы писали:
MZ>При этом после преобразования к указателю на метод класса-предка данная функция-член MZ>будет вызываться только с объектами, являющимися объектами этого оконного класса или его классов-наследников. MZ>Это обеспечивается логикой обработки сообщений внутри MFC.
Это работает, потому что они требуют, чтобы CObject был первым классом в цепочке наследования. Как только ты изменишь порядок, у тебя начнется песня.
Здравствуйте, PavelCH, Вы писали:
R>>Так тебе предложили вариант. Ты не заметил сделанных изменений? PCH>Ссори, не заметил. PCH>Но с виртуалом немного не то. Я не знаю сколько и каких методов будет в наследуемых классах. Мне их надо вызывать по текстовой строке.
struct ca
{
struct command
{
const char* name;
ca::proc proc;
};
int a, b;
ca() : a(0), b(0)
{
}
virtual void call_by_name( const char *name ) {};
};
struct cb : ca
{
void f1()
{
a++; b++;
}
void f2()
{
a++; b--;
}
virtual void call_by_name( const char *name )
{
// В зависимости от name вызвать нужную функцию или call_by_name() из базового класса.
...
}
static command commands[];
};
struct cс : cb
{
int c;
void f3()
{
b++; c++;
}
virtual void call_by_name( const char *name )
{
...
}
static command commands[];
};
ca* p;
p->call_by_name( "f3" );
Здравствуйте, PavelCH, Вы писали:
PCH>Чем опасен такой код:
PCH> {"f2", (ca::proc)&cb::f2}, // Тут опасный код
PCH> {"func", &ca::func},
PCH>(p->*pdata[0].proc)(); // Есть ли какие-то подводные камни в таком вызове?
Нужно с самого начала понять следующее: указатели на объекты безопасно кастятся вверх по иерархии, от наследников к предкам; а указатели на функции — наоборот, от предков к наследникам.
Это следует из принципа подстановки Лисков.
// не будем сейчас про функции-члены, рассмотрим свободныеvoid foo(base* p);
void bar(derived* p);
// как написать адаптер foo с сигнатурой, как у bar?void foo_downcasted(derived* p) { foo(p); } // очевидно! и безо всяких трюков! LSP выполняется
// как написать адаптер bar с сигнатурой, как у foo?void bar_upcasted(base* p) { bar(somehow_explicitly_cast<derived*>(p)); } // а вот тут LSP нарушен, придётся что-то делать...
Главная проблема заключается в том, что у иерархии классов база одна (ну хорошо, может быть несколько, — но они зафиксированы), а наследников — сколько угодно.
Поэтому, имея дело с указателями на функции, придётся волюнтаристски закрепить какой-то один класс в роли "самого нижнего наследника".
Соответственно, тип "указатель на функцию с аргументом (this) этого класса" становится конечным и включает только соответствующие функции этого наследника и всех его предков.
Если мы хотим бОльшего разнообразия, то придётся прибегнуть к... да! старым добрым виртуальным функциям. В которых даункастинг не ломает LSP, потому что реализация функции заведомо согласована с фактическим классом объекта.
То есть, получается следующее. Наш "самый нижний наследник", на самом деле, оказывается самым базовым классом с виртуальными функциями. От которого наследуются "ниже нижнего".
Однако, цена вопроса — многократная косвенность. Сперва получаем указатель на адаптер функции (приводящий тип к "нижнему" — сдвиг базы, вот это всё...), из которого вызывается виртуальная функция, которая лезет в vptr, затем в vtable, затем в адаптер реализации (ещё раз сдвиг базы...), и наконец, вызывает конечную функцию.
Если программист может дать какие-то железные гарантии, что указатель на объект и указатель на функцию согласованы по типам, то можно всё нафиг посокращать и писать опасный код.
Возможно, — с корректным кастингом, пусть и явным.
struct base {
void foo();
static void free_foo(base* p) { p->foo(); }
void goo();
static void free_goo(base* p) { p->goo(); }
};
struct derived : base {
void bar();
static void free_bar(base* p) { static_cast<derived*>(p)->bar(); }
};
base* p = new derived();
void (*f)(base*) = derived::free_foo;
void (*b)(base*) = derived::free_bar;
// здесь волшебным образом даны гарантии, что f и b вызовутся только при p, являющимся derived (или его субклассом)
f(p);
b(p);
base* q = new base();
void (*F)(base*) = base::free_foo;
void (*G)(base*) = base::free_goo;
// здесь даны гарантии только о субклассе base
F(q);
G(q);
В некотором смысле, это всё равно виртуальные функции, только рукосипедные.
Здравствуйте, PavelCH, Вы писали:
PCH>Чем опасен такой код:
По стандарту норм (ссылки давали), но с оговорками. Можно сделать апкаст, если ты сможешь сделать даункаст, который нельзя сделать, если:
An rvalue of type “pointer to member of B of type cv T,” where B is a class type, can be converted to an rvalue 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, a program that necessitates this conversion is ill-formed.
Если у тебя нет можественных вхождений базы или виртуальной базы, и база при этом доступна наследникам, то будет ок. В случае чего, словишь ошибку компиляции и придется все переделать.
Самый большой подводный камень здесь — это метод каста. Ты написал Си-каст (скобочки), однако здесь подходит исключительно static_cast. Если он не проходит из-за того, что описано выше, то ничего поделать нельзя и надо думать над другим решением. Однако, у тебя оно пойдет по пути reinterpret_cast, который не добавляет никаких смещений в указатель на метод класса, в каких-то ситуациях порождая невалидный this, отчего всенепременно случится салют.
Вот и думай, насколько ты можешь контролировать этот каст в наследниках? Насколько ты готов отлавливать любителей писать reinterpret_cast или C-style cast? Может, обертку какую написать со статик кастом...
Дополнение:
gcc по умолчанию нормально обрабатывает варианты множественного наследования и выдает правильный указательна метод класса, с правильным смещением this. А вот VC++ грешит незаконной оптимизацией, которую в случае множественного наследования придется отключать ключом /vmg.