Вызов функции класса по ссылке
От: PavelCH  
Дата: 24.11.17 08:19
Оценка:
Добрый день!

Чем опасен такой код:

struct ca
{
    int a, b;
    ca() : a(0), b(0)
    {
    }
    void func()
    {
        a++;
    }
    typedef void(ca::*proc)();
};
struct cb : ca
{
    int c;
    cb() : c(0)
    {
    }
    void f2()
    {
        b++; c++;
    }
};
static struct routine
{
    const char*    name;
    ca::proc        proc;
} pdata[] = {
    {"f2", (ca::proc)&cb::f2}, // Тут опасный код
    {"func", &ca::func},
};
cb t1;
ca* p = &t1;
(p->*pdata[0].proc)(); // Есть ли какие-то подводные камни в таком вызове?
Нехай щастить
Re: Вызов функции класса по ссылке
От: Анатолий Широков СССР  
Дата: 24.11.17 08:51
Оценка:
Что мешает сделать без подводных камней?

#include <iostream>
using namespace std;

struct ca
{
    int a, b;
    ca() : a(0), b(0)
    {
    }
    virtual void f2() = 0;
    void func()
    {
        a++;
    }
    typedef void(ca::*proc)();
};
struct cb : ca
{
    int c;
    cb() : c(0)
    {
    }
    void f2() override
    {
        b++; c++;
    }
};

static struct routine
{
    const char*    name;
    ca::proc        proc;
} pdata[] = {
    {"f2", &ca::f2}, 
    {"func", &ca::func},
};
cb t1;
ca* p = &t1;
(p->*pdata[0].proc)();
Отредактировано 24.11.2017 8:55 Анатолий Широков . Предыдущая версия . Еще …
Отредактировано 24.11.2017 8:52 Анатолий Широков . Предыдущая версия .
Re[2]: Вызов функции класса по ссылке
От: PavelCH  
Дата: 24.11.17 08:58
Оценка:
АШ>Что мешает сделать без подводных камней?

А как правильно сделать?
Суть задачи — есть базовый класс. В ней есть метод в стиле: пробежаться по структуре, найти по имени метод который нужно вызывать и вызвать его. Проблема в том, что методы, которые надо вызывать определяются в наследуемых классах.
Нехай щастить
Re[3]: Вызов функции класса по ссылке
От: rg45 СССР  
Дата: 24.11.17 10:12
Оценка:
Здравствуйте, PavelCH, Вы писали:

АШ>>Что мешает сделать без подводных камней?


PCH>А как правильно сделать?


Так тебе предложили вариант. Ты не заметил сделанных изменений?
--
Не можешь достичь желаемого — пожелай достигнутого.
Re[4]: Вызов функции класса по ссылке
От: PavelCH  
Дата: 24.11.17 10:23
Оценка:
R>Так тебе предложили вариант. Ты не заметил сделанных изменений?
Ссори, не заметил.
Но с виртуалом немного не то. Я не знаю сколько и каких методов будет в наследуемых классах. Мне их надо вызывать по текстовой строке.
Например, надо вот такое:
struct ca
{
    typedef void(ca::*proc)();
    struct command
    {
        const char*    name;
        ca::proc        proc;
    };
    int a, b;
    ca() : a(0), b(0)
    {
    }
};
struct cb : ca
{
    void f1()
    {
        a++; b++;
    }
    void f2()
    {
        a++; b--;
    }
    static command commands[];
};
struct cс : cb
{
    int c;
    void f3()
    {
        b++; c++;
    }
    static command commands[];
};
ca::command cb::commands[] = {
    {"f1", (ca::proc)&f1}, 
    {"f2", (ca::proc)&f2},
};
ca::command cc::commands[] = {
    {"f1", (ca::proc)&f1}, 
    {"f2", (ca::proc)&f2},
    {"f3", (ca::proc)&f3},
};
cc t1;
ca* p = &t1;
(p->*cc::commands[0].proc)();
Нехай щастить
Re: Вызов функции класса по ссылке
От: N. I.  
Дата: 24.11.17 10:24
Оценка: 5 (2) +1
PavelCH:

    {"f2", (ca::proc)&cb::f2}, // Тут опасный код

(p->*pdata[0].proc)(); // Есть ли какие-то подводные камни в таком вызове?

Формально такое должно работать.
http://eel.is/c++draft/expr.static.cast#12
http://eel.is/c++draft/expr.mptr.oper

Правильно ли это будет работать при использовании какого-то конкретного компилятора, надо проверять отдельно.
Re[5]: Вызов функции класса по ссылке
От: rg45 СССР  
Дата: 24.11.17 10:55
Оценка:
Здравствуйте, PavelCH, Вы писали:

R>>Так тебе предложили вариант. Ты не заметил сделанных изменений?

PCH>Ссори, не заметил.
PCH>Но с виртуалом немного не то. Я не знаю сколько и каких методов будет в наследуемых классах. Мне их надо вызывать по текстовой строке.
PCH>Например, надо вот такое:
PCH> . . .

Опасность такого дизайна в том, что он свободно позволяет вызывать функции-члены классов для объеков, не являющихся экземплярами этих классов. Неопределенное поведение и трудно диагностируемые ошибки. Возможно, тебе имеет смысл посмотреть в сторону концепций и статического полиморфизма.
--
Не можешь достичь желаемого — пожелай достигнутого.
Re[6]: Вызов функции класса по ссылке
От: PavelCH  
Дата: 24.11.17 11:15
Оценка:
R>Опасность такого дизайна в том, что он свободно позволяет вызывать функции-члены классов для объеков, не являющихся экземплярами этих классов. Неопределенное поведение и трудно диагностируемые ошибки. Возможно, тебе имеет смысл посмотреть в сторону концепций и статического полиморфизма.
Тут проблема в том что я не знаю конечного набора функций которые могут быть в наследуемых классах и которые надо будет вызывать. Есть какие-то рекомендации как решать подобные проблемы?
Нехай щастить
Re[7]: Вызов функции класса по ссылке
От: rg45 СССР  
Дата: 24.11.17 12:20
Оценка: 2 (1) +2
Здравствуйте, PavelCH, Вы писали:

PCH>Тут проблема в том что я не знаю конечного набора функций которые могут быть в наследуемых классах и которые надо будет вызывать. Есть какие-то рекомендации как решать подобные проблемы?


Мне трудно что-то посоветовать. Полагаю, в такой постановке, задача не имеет безопасного решения. Я бы в первую очередь попытался понять, каким образом возникла такая портребность. Весьма вероятно, это результат какой-нибуль другой ошибки дизайна.
--
Не можешь достичь желаемого — пожелай достигнутого.
Re[5]: Вызов функции класса по ссылке
От: Анатолий Широков СССР  
Дата: 24.11.17 13:54
Оценка:
Здравствуйте, PavelCH, Вы писали:

R>>Так тебе предложили вариант. Ты не заметил сделанных изменений?

PCH>Ссори, не заметил.
PCH>Но с виртуалом немного не то. Я не знаю сколько и каких методов будет в наследуемых классах. Мне их надо вызывать по текстовой строке.
PCH>Например, надо вот такое:
PCH>
PCH>struct ca
PCH>{
PCH>cc t1;
PCH>ca* p = &t1;
PCH>(p->*cc::commands[0].proc)();
PCH>


А чем продиктован этот дизайн? Проблема вызова произвольной функции-члена в произвольной точке иерархии наследования решается наивно, но безопасно:

#include <cassert>
#include <iostream>
#include <map>
#include <string_view>
 
struct base {
   virtual ~base() = default;
   virtual void invoke(std::string_view) = 0;
};
 
struct der1: base {
   void invoke(std::string_view method) override {
       static std::map<std::string_view, void (der1::*)()> commands = {
           {"f1", &der1::f1},
           {"f2", &der1::f2},
       };
       auto it = commands.find(method);
       assert(it != commands.end());
       (this->*it->second)();
   }
   void f1() {std::cout << "der1::f1" << std::endl;}
   void f2() {std::cout << "der1::f2" << std::endl;}
};

struct der2: base {
    void invoke(std::string_view method) override {
       static std::map<std::string_view, void (der2::*)()> commands = {
           {"f1", &der2::f1},
           {"f2", &der2::f2},
           {"f3", &der2::f3},
       };
       auto it = commands.find(method);
       assert(it != commands.end());
       (this->*it->second)();
   }
   void f1() {std::cout << "der2::f1" << std::endl;}
   void f2() {std::cout << "der2::f2" << std::endl;}
   void f3() {std::cout << "der2::f3" << std::endl;}
};
 
int main() {
    base* b = nullptr;
    der1 d1;
    b = &d1;
    b->invoke("f1");
    der2 d2;
    b = &d2;
    b->invoke("f2");
Re[6]: Вызов функции класса по ссылке
От: PavelCH  
Дата: 24.11.17 14:18
Оценка:
Здравствуйте, Анатолий Широков, Вы писали:

Думал в этом направлении. Но при таком подходе:

1) Не хочется писать "суппорт" код по поиску метода по строке и вызову его в каждом наследуемом классе. Но на самом деле это мелочь. Больше по п.2
2) Нет возможности получить список методов класса в формализованной однотипной структуре. То есть по условиям моей задачи надо у объекта также иметь список того что он может.
Нехай щастить
Re[5]: Вызов функции класса по ссылке
От: Lexey Россия  
Дата: 24.11.17 16:17
Оценка:
Здравствуйте, PavelCH, Вы писали:

PCH>Но с виртуалом немного не то. Я не знаю сколько и каких методов будет в наследуемых классах. Мне их надо вызывать по текстовой строке.


+1 к вопросу rg45.
В качестве варианта могу предложить хранить не ca::proc, а лямбду, завернутую в std::function<ca*>.
Лямбда для f2 будет такой:
[](ca* base)
{
    dynamic_cast<cb&>(*base).f2();
}

Так хотя бы в рантайме будет защита от попытки подсунуть указатель на cc, а не на cb.
"Будь достоин победы" (c) 8th Wizard's rule.
Отредактировано 24.11.2017 16:19 Lexey . Предыдущая версия . Еще …
Отредактировано 24.11.2017 16:18 Lexey . Предыдущая версия .
Отредактировано 24.11.2017 16:18 Lexey . Предыдущая версия .
Re: Вызов функции класса по ссылке
От: MasterZiv СССР  
Дата: 01.12.17 08:54
Оценка:
Здравствуйте, 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

Соответственно, в части вариантов вызовов нарушается принцип подстановки и программа может быть невалидной.
Именно поэтому приведение указателя на метод или член класса-наследника к указателю на метод или член класса-предка
является неопределённым поведением (не проверял вот прямо сейчас в стандарте, но уверен, что именно так дело и обстоит).

Т.е. так писать программу нельзя.

Однако, если метод класса-наследника, который приведён к указателю на метод класса-предка, не обращается
к нестатическим данным класса-наследника
и не вызывает нестатические методы класса-наследника, которые в свою очередь
могут иметь доступ к нестатическим данным класса-наследника, или после преобразования к указателю на
метод класса-предка через этот указатель метод будет вызываться только с объектами, являющимися объектами
класса-наследника
, метод наследника может нормально работать.
Отредактировано 01.12.2017 8:56 MasterZiv . Предыдущая версия .
Re[2]: Вызов функции класса по ссылке
От: MasterZiv СССР  
Дата: 01.12.17 09:07
Оценка: 2 (1)
Здравствуйте, MasterZiv, Вы писали:

MZ>Т.е. так писать программу нельзя.


Реально такое преобразование применяется и работает на практике, например, в MFC, таким образом там
оформляются карты сообщений оконных классов. Методы классов приводятся к указателям на методы
базового класса CCmdTarget.

typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);


При этом после преобразования к указателю на метод класса-предка данная функция-член
будет вызываться только с объектами, являющимися объектами этого оконного класса или его классов-наследников.
Это обеспечивается логикой обработки сообщений внутри MFC.
Re[3]: Вызов функции класса по ссылке
От: Анатолий Широков СССР  
Дата: 01.12.17 12:23
Оценка:
Здравствуйте, MasterZiv, Вы писали:

MZ>При этом после преобразования к указателю на метод класса-предка данная функция-член

MZ>будет вызываться только с объектами, являющимися объектами этого оконного класса или его классов-наследников.
MZ>Это обеспечивается логикой обработки сообщений внутри MFC.

Это работает, потому что они требуют, чтобы CObject был первым классом в цепочке наследования. Как только ты изменишь порядок, у тебя начнется песня.
Re[4]: Вызов функции класса по ссылке
От: PavelCH  
Дата: 01.12.17 13:48
Оценка:
АШ>Это работает, потому что они требуют, чтобы CObject был первым классом в цепочке наследования.
Да, именно так я хочу и сделать.
Нехай щастить
Re[5]: Вызов функции класса по ссылке
От: AleksandrN Россия  
Дата: 01.12.17 14:12
Оценка:
Здравствуйте, 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" );
Re: Вызов функции класса по ссылке
От: Кодт Россия  
Дата: 09.12.17 13:17
Оценка: 2 (1)
Здравствуйте, 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);


В некотором смысле, это всё равно виртуальные функции, только рукосипедные.
Перекуём баги на фичи!
Re: Вызов функции класса по ссылке
От: andrey.desman  
Дата: 09.12.17 14:05
Оценка: 2 (1)
Здравствуйте, 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.

А чего тебе std::function не подходит?
Отредактировано 09.12.2017 14:20 andrey.desman . Предыдущая версия . Еще …
Отредактировано 09.12.2017 14:18 andrey.desman . Предыдущая версия .
Re[2]: Вызов функции класса по ссылке
От: PavelCH  
Дата: 10.12.17 06:43
Оценка:
Здравствуйте, andrey.desman, Вы писали:
Отдельное спасибо за static_cast<>.
Нехай щастить
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.