Вызов методов класса C ++ через указатель на функцию

113

Как получить указатель на функцию для функции-члена класса, а затем вызвать эту функцию-член с конкретным объектом? Я хочу написать:

class Dog : Animal
{
    Dog ();
    void bark ();
}


Dog* pDog = new Dog ();
BarkFunction pBark = &Dog::bark;
(*pBark) (pDog);

Также, если возможно, я хотел бы вызвать конструктор и через указатель:

NewAnimalFunction pNew = &Dog::Dog;
Animal* pAnimal = (*pNew)();    

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

Тони Пони
источник
1
Я до сих пор не совсем понимаю «почему», если вы хотите вызвать функцию-член объекта, а затем просто передать указатель на объект? Если люди жалуются на это, поскольку это позволяет лучше инкапсулировать класс, почему бы не создать интерфейсный класс, от которого наследуются все классы?
Чад
Это может быть полезно при реализации чего-то вроде шаблона команды, хотя многие люди будут использовать boost :: function, чтобы скрыть механизм необработанного указателя на член.
CB Bailey
9
Почему вы распределяете эту собаку динамически? Затем вам также необходимо вручную удалить объект. Это очень похоже на то, что вы пришли с Java, C # или другого сопоставимого языка и все еще боретесь с C ++. Простой автоматический объект ( Dog dog;), скорее всего, то, что вам нужно.
sbi
1
@Chad: Я бы в основном согласился, но бывают случаи, когда передача ссылки обходится дороже. Рассмотрим цикл, который выполняет итерацию по некоторому типу данных (синтаксический анализ, вычисление и т. Д.), Чем возможность вызова функции на основе некоторых вычислений if / else налагает затраты, когда простой вызов указанной тоже функции может избежать такого if / then / else проверяет возможность выполнения этих проверок перед входом в цикл.
Эрик

Ответы:

127

Прочтите это для подробностей:

// 1 define a function pointer and initialize to NULL

int (TMyClass::*pt2ConstMember)(float, char, char) const = NULL;

// C++

class TMyClass
{
public:
   int DoIt(float a, char b, char c){ cout << "TMyClass::DoIt"<< endl; return a+b+c;};
   int DoMore(float a, char b, char c) const
         { cout << "TMyClass::DoMore" << endl; return a-b+c; };

   /* more of TMyClass */
};
pt2ConstMember = &TMyClass::DoIt; // note: <pt2Member> may also legally point to &DoMore

// Calling Function using Function Pointer

(*this.*pt2ConstMember)(12, 'a', 'b');
Сатбир
источник
24
Удивительно, что решили, что так: *this.*pt2Memberбудет работать. *имеет больший приоритет над .*... Лично я бы все равно написал this->*pt2Member, что на одного оператора меньше.
Alexis Wilke
7
Почему вы должны инициализировать pt2ConstMemberдо NULL?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@AlexisWilke, почему это удивительно? Для прямых объектов (не указателей) это (object.*method_pointer)так, поэтому мы хотим, *чтобы у них был больший приоритет.
Чиро Сантилли 郝海东 冠状 病 六四 事件 法轮功 01
@ TomášZato, если я не ошибаюсь (а возможно, и ошибаюсь), thisпросто используется для демонстрации того, что все, к чему вы .*относитесь, должно быть указателем на экземпляр (подкласса). Однако для меня это новый синтаксис, я только предполагаю, основываясь на других ответах и ​​ресурсах, связанных здесь. Я предлагаю отредактировать, чтобы сделать это более ясным.
c1moore
1
Поздравляю со 100!
Джонатан Ми
59

Как получить указатель на функцию для функции-члена класса, а затем вызвать эту функцию-член с конкретным объектом?

Проще всего начать с файла typedef. Для функции-члена вы добавляете имя класса в объявление типа:

typedef void(Dog::*BarkFunction)(void);

Затем для вызова метода вы используете ->*оператор:

(pDog->*pBark)();

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

Я не верю, что с такими конструкторами можно работать - ctors и dtors особенные. Обычный способ добиться такого рода вещей - использовать фабричный метод, который в основном представляет собой статическую функцию, вызывающую за вас конструктор. См. Пример кода ниже.

Я изменил ваш код, чтобы в основном делать то, что вы описываете. Ниже приведены некоторые предостережения.

#include <iostream>

class Animal
{
public:

    typedef Animal*(*NewAnimalFunction)(void);

    virtual void makeNoise()
    {
        std::cout << "M00f!" << std::endl;
    }
};

class Dog : public Animal
{
public:

    typedef void(Dog::*BarkFunction)(void);

    typedef Dog*(*NewDogFunction)(void);

    Dog () {}

    static Dog* newDog()
    {
        return new Dog;
    }

    virtual void makeNoise ()
    {
        std::cout << "Woof!" << std::endl;
    }
};

int main(int argc, char* argv[])
{
    // Call member function via method pointer
    Dog* pDog = new Dog ();
    Dog::BarkFunction pBark = &Dog::makeNoise;

    (pDog->*pBark)();

    // Construct instance via factory method
    Dog::NewDogFunction pNew = &Dog::newDog;

    Animal* pAnimal = (*pNew)();

    pAnimal->makeNoise();

    return 0;
}

Теперь, хотя вы обычно можете использовать Dog*вместо знака Animal*благодарности магию полиморфизма, тип указателя на функцию не соответствует правилам поиска иерархии классов. Таким образом, указатель на метод Animal несовместим с указателем на метод Dog, другими словами, вы не можете присвоить Dog* (*)()a переменной типа Animal* (*)().

Статический newDogметод - это простой пример фабрики, которая просто создает и возвращает новые экземпляры. Будучи статической функцией, она имеет обычный typedef(без классификатора).

Ответив на вышесказанное, мне интересно, нет ли лучшего способа добиться того, что вам нужно. Есть несколько конкретных сценариев, в которых вы бы поступили подобным образом, но вы можете обнаружить, что есть другие шаблоны, которые лучше подходят для вашей проблемы. Если вы опишете в более общих выражениях, чего пытаетесь достичь, коллективный разум может оказаться еще более полезным!

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

Гавинб
источник
10
Я использую C ++ более 10 лет и постоянно учусь чему-то новому. Я никогда не слышал об этом ->*раньше, но теперь я надеюсь, что мне это никогда не понадобится :)
Томас
31

Я не думаю, что кто-то объяснил здесь, что одна из проблем заключается в том, что вам нужны « указатели на элементы », а не обычные указатели на функции.

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

Сказав, что они вам нужны, теперь я скажу, что вам действительно нужно их избегать. Серьезно, указатели на члены - это боль. Гораздо разумнее взглянуть на объектно-ориентированные шаблоны проектирования, которые достигают той же цели, или использовать boost::functionили что-то еще, как упомянуто выше, если вы сделаете этот выбор, то есть.

Если вы предоставляете этот указатель на функцию существующему коду, поэтому вам действительно нужен простой указатель на функцию, вам следует написать функцию как статический член класса. Статическая функция-член не понимает this, поэтому вам нужно передать объект в качестве явного параметра. Когда-то была довольно необычная идиома в этих строках для работы со старым кодом C, которому нужны указатели на функции.

class myclass
{
  public:
    virtual void myrealmethod () = 0;

    static void myfunction (myclass *p);
}

void myclass::myfunction (myclass *p)
{
  p->myrealmethod ();
}

Поскольку myfunctionна самом деле это обычная функция (не считая проблем с областью видимости), указатель на функцию можно найти обычным способом C.

РЕДАКТИРОВАТЬ - этот вид метода называется «методом класса» или «статической функцией-членом». Основное отличие от функции, не являющейся членом, состоит в том, что если вы ссылаетесь на нее извне класса, вы должны указать область с помощью ::оператора разрешения области. Например, чтобы получить указатель на функцию, используйте &myclass::myfunctionи для его вызова используйте myclass::myfunction (arg);.

Подобные вещи довольно распространены при использовании старых API Win32, которые изначально были разработаны для C, а не C ++. Конечно, в этом случае параметром обычно является LPARAM или аналогичный, а не указатель, и требуется некоторое приведение типов.

Steve314
источник
«myfunction» не является нормальной функцией, если под нормальным вы подразумеваете функцию в стиле C. myfunction точнее назвать методом myclass. Методы класса не похожи на обычные функции тем, что в них есть что-то, чего функция в стиле C не делает, а именно указатель this.
Эрик
3
совет использовать буст сурово. Есть практические веские причины для использования указателей на методы. Я не возражаю против упоминания повышения в качестве альтернативы, но ненавижу, когда кто-то говорит, что кто-то должен использовать его, не зная всех фактов. За повышение приходится платить! А если это встраиваемая платформа, то такой выбор может оказаться невозможным. Кроме того, мне очень нравится твоя статья.
Эрик
@Eric - Что касается вашего второго пункта, я не собирался говорить «ты должен использовать Boost», и на самом деле я сам никогда не использовал Boost. Намерение (насколько мне известно через 3 года) состояло в том, чтобы люди искали альтернативы и перечисляли несколько возможностей. «Или что угодно» означает, что список не является исчерпывающим. Указатели на элементы требуют удобочитаемости. Их краткое представление источника может также скрыть затраты времени выполнения - в частности, указатель члена на метод должен работать как с невиртуальными, так и с виртуальными методами и должен знать, какие из них.
Steve314
@Eric - Не только это, но и эти проблемы являются причиной непереносимости указателей на элементы - Visual C ++, по крайней мере в прошлом, требовались дополнительные подсказки о том, как представлять типы указателей на элементы. Я бы использовал подход статических функций для встроенной системы - представление указателя такое же, как и для любого другого указателя на функцию, затраты очевидны и нет проблем с переносимостью. И вызов, заключенный в статическую функцию-член, знает (во время компиляции), является ли вызов виртуальным или нет - никаких проверок времени выполнения не требуется, кроме обычных поисков vtable для виртуальных методов.
Steve314,
@Eric - по вашему первому пункту - я знаю, что статическая функция-член - это не совсем то же самое, что функция в стиле C (отсюда «проблемы с областью видимости»), но мне, вероятно, следовало указать имя.
Steve314,
18
typedef void (Dog::*memfun)();
memfun doSomething = &Dog::bark;
....
(pDog->*doSomething)(); // if pDog is a pointer
// (pDog.*doSomething)(); // if pDog is a reference
Арак
источник
2
Должно быть: (pDog -> * doSomething) (); // если pDog является указателем // (pDog. * doSomething) (); // если pDog является ссылкой, поскольку оператор () имеет более высокий приоритет, то -> * и. *.
Tomek
12

Минимальный запускаемый пример

main.cpp

#include <cassert>

class C {
    public:
        int i;
        C(int i) : i(i) {}
        int m(int j) { return this->i + j; }
};

int main() {
    // Get a method pointer.
    int (C::*p)(int) = &C::m;

    // Create a test object.
    C c(1);
    C *cp = &c;

    // Operator .*
    assert((c.*p)(2) == 3);

    // Operator ->*
    assert((cp->*p)(2) == 3);
}

Скомпилируйте и запустите:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

Протестировано в Ubuntu 18.04.

Вы не можете изменить порядок скобок или опустить их. Следующие действия не работают:

c.*p(2)
c.*(p)(2)

GCC 9.2 завершится ошибкой:

main.cpp: In function int main()’:
main.cpp:19:18: error: must use ‘.*’ or ‘->*’ to call pointer-to-member function in p (...)’, e.g. ‘(... ->* p) (...)’
   19 |     assert(c.*p(2) == 3);
      |

Стандарт C ++ 11

.*и ->*являются одиночными операторами, введенными в C ++ для этой цели и отсутствующими в C.

Стандартный проект C ++ 11 N3337 :

  • 2.13 «Операторы и знаки препинания» содержит список всех операторов, который содержит .*и ->*.
  • 5.5 "Операторы указателя на член" объясняют, что они делают.
Чиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
источник
11

Я пришел сюда, чтобы узнать, как создать указатель на функцию (а не указатель на метод) из метода, но ни один из ответов здесь не дает решения. Вот что я придумал:

template <class T> struct MethodHelper;
template <class C, class Ret, class... Args> struct MethodHelper<Ret (C::*)(Args...)> {
    using T = Ret (C::*)(Args...);
    template <T m> static Ret call(C* object, Args... args) {
        return (object->*m)(args...);
    }
};

#define METHOD_FP(m) MethodHelper<decltype(m)>::call<m>

Итак, для вашего примера вы бы теперь сделали:

Dog dog;
using BarkFunction = void (*)(Dog*);
BarkFunction bark = METHOD_FP(&Dog::bark);
(*bark)(&dog); // or simply bark(&dog)

Изменить:
используя С ++ 17, есть еще лучшее решение:

template <auto m> struct MethodHelper;
template <class C, class Ret, class... Args, Ret (C::*m)(Args...)> struct MethodHelper<m> {
    static Ret call(C* object, Args... args) {
        return (object->*m)(args...);
    }
};

который можно использовать напрямую без макроса:

Dog dog;
using BarkFunction = void (*)(Dog*);
BarkFunction bark = MethodHelper<&Dog::bark>::call;
(*bark)(&dog); // or simply bark(&dog)

Для методов с такими модификаторами, как constвам могут потребоваться дополнительные специализации, например:

template <class C, class Ret, class... Args, Ret (C::*m)(Args...) const> struct MethodHelper<m> {
    static Ret call(const C* object, Args... args) {
        return (object->*m)(args...);
    }
};
ресница
источник
6

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

Чтобы вызвать функцию-член, вам нужно знать две вещи:

  • Какую функцию-член вызывать
  • Какой экземпляр следует использовать (чья функция-член)

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

hrnt
источник
1
Я проголосовал за это, но хотел бы добавить пояснение на случай, если OP не знает, о чем вы говорите, «какой экземпляр». Я бы расширил, чтобы объяснить присущий указателю «это».
Эрик
6

Указатель функции на член класса - это проблема, которая действительно подходит для использования boost :: function. Небольшой пример:

#include <boost/function.hpp>
#include <iostream>

class Dog 
{
public:
   Dog (int i) : tmp(i) {}
   void bark ()
   {
      std::cout << "woof: " << tmp << std::endl;
   }
private:
   int tmp;
};



int main()
{
   Dog* pDog1 = new Dog (1);
   Dog* pDog2 = new Dog (2);

   //BarkFunction pBark = &Dog::bark;
   boost::function<void (Dog*)> f1 = &Dog::bark;

   f1(pDog1);
   f1(pDog2);
}
Вениамин
источник
2

Чтобы создать новый объект, вы можете либо использовать новое размещение, как упомянуто выше, либо реализовать в вашем классе метод clone (), который создает копию объекта. Затем вы можете вызвать этот метод клонирования с помощью указателя на функцию-член, как описано выше, для создания новых экземпляров объекта. Преимущество клонирования заключается в том, что иногда вы можете работать с указателем на базовый класс, когда вы не знаете тип объекта. В этом случае проще использовать метод clone (). Кроме того, clone () позволит вам скопировать состояние объекта, если вы этого хотите.

Корвин Джой
источник
клоны могут быть дорогими, и OP может пожелать избежать их, если производительность является проблемой или вызывает некоторые опасения.
Эрик
0

Я сделал это с помощью std :: function и std :: bind ..

Я написал этот класс EventManager, который хранит вектор обработчиков в unordered_map, который сопоставляет типы событий (которые являются просто const unsigned int, у меня их большое перечисление в области имен) на вектор обработчиков для этого типа события.

В моем классе EventManagerTests я настроил обработчик событий, например:

auto delegate = std::bind(&EventManagerTests::OnKeyDown, this, std::placeholders::_1);
event_manager.AddEventListener(kEventKeyDown, delegate);

Вот функция AddEventListener:

std::vector<EventHandler>::iterator EventManager::AddEventListener(EventType _event_type, EventHandler _handler)
{
    if (listeners_.count(_event_type) == 0) 
    {
        listeners_.emplace(_event_type, new std::vector<EventHandler>());
    }
    std::vector<EventHandler>::iterator it = listeners_[_event_type]->end();
    listeners_[_event_type]->push_back(_handler);       
    return it;
}

Вот определение типа EventHandler:

typedef std::function<void(Event *)> EventHandler;

Затем снова в EventManagerTests :: RaiseEvent я делаю следующее:

Engine::KeyDownEvent event(39);
event_manager.RaiseEvent(1, (Engine::Event*) & event);

Вот код для EventManager :: RaiseEvent:

void EventManager::RaiseEvent(EventType _event_type, Event * _event)
{
    if (listeners_.count(_event_type) > 0)
    {
        std::vector<EventHandler> * vec = listeners_[_event_type];
        std::for_each(
            begin(*vec), 
            end(*vec), 
            [_event](EventHandler handler) mutable 
            {
                (handler)(_event);
            }
        );
    }
}

Это работает. Я получаю вызов в EventManagerTests :: OnKeyDown. Мне нужно удалить векторы, наступает время очистки, но как только я это сделаю, утечек не будет. Создание события на моем компьютере занимает около 5 микросекунд, это примерно в 2008 году. Не совсем быстро, но. Достаточно справедливо, если я знаю это и не использую это в сверхгорячем коде.

Я хотел бы ускорить его, свернув свои собственные std :: function и std :: bind, и, возможно, используя массив массивов, а не unordered_map векторов, но я не совсем понял, как сохранить функцию-член указатель и вызовите его из кода, который ничего не знает о вызываемом классе. Ответ Ресницы выглядит очень интересно ..

Shavais
источник