Nano Hash - криптовалюты, майнинг, программирование

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

У меня есть класс игрока, который выглядит так (урезанный до необходимого для этой проблемы):

class Player
{
    public:
        Player();
        ~Player();

        void kill();
        void death();
        void reset();
};

Функции kill(), death() и reset() выглядят следующим образом:

void Player::kill()
{
    void (*dPtr)() = &death;

    Game::idle(dPtr, 48);
}

void Player::death()
{
    reset();
}

void Player::reset()
{
    //resets
}

Функция idle — это статическая функция-член Game, которая принимает указатель на функцию и целое число n и вызывает функцию после n тиков. Вот функция, реализация не имеет значения:

class Game {
    static void idle(void (*)(), int);
};

Этот код дает мне ошибку:

ISO C++ forbids taking the address of an unqualified or parenthesized non-static member function to form a pointer to member function.  Say '&Player::death' [-fpermissive]

Поэтому я меняю строку с

    void (*dPtr)() = &death;

to

    void (Player::*dPtr)() = &Player::death;

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

no matching function for call to 'Game::idle(void (Player::*&)(), int)'

Итак, мой вопрос: как я могу передать указатель функции-члена Player::*dPtr в функцию ожидания, которая принимает void (*)() в качестве аргумента?

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


  • Взгляните на boost::bind и boost::function. 15.03.2015
  • Используйте std::bind(), в текущем стандарте нет необходимости в ускорении. 15.03.2015
  • @πάνταῥεῖ Объект, возвращаемый std::bind, несовместим по присваиванию с void (*)(). 15.03.2015
  • @zwol Итак, функцию static void idle(void (*)(), int); нужно изменить. В чем проблема? 16.03.2015
  • Можете ли вы изменить интерфейс idle? 16.03.2015
  • @πάνταῥεῖ Ну, его должен изменить, но нет хорошего способа изменить его, чтобы сделать его совместимым с std::bind, учитывая другие ограничения, которые, как я полагаю, присутствуют здесь (а именно, Game::idle не может быть шаблон). 16.03.2015
  • Содержит ли указатель функции информацию о вызывающем его объекте? 16.03.2015
  • @Yaxlat Нет, это не так. Вот и вся ваша проблема в двух словах. 16.03.2015
  • О да, тогда это было ложным предположением. Я попробую ваш ответ. Это напоминает мне много функций Python с передачей себя в качестве объекта. 16.03.2015

Ответы:


1

Чтобы вызвать функцию-член с помощью указателя, вам нужны два указателя: указатель на саму функцию и указатель на объект, который должен быть this. Ваш Game::idle API не поддерживает такое использование. Вам нужно изменить его так, чтобы он передавал хотя бы один аргумент (условно типа void *) обратному вызову. Затем вы можете использовать следующий шаблон:

struct Player
{
        // ...
        void kill();

        // ...
        static void call_kill(void *self);
};

void Player::call_kill(void *self)
{
    static_cast<Player *>(self)->kill();
}

struct Game
{
    static void idle(void (*)(void *), void *, int);
};

void Game::idle(void (*callback)(void *), void *arg, int ticks)
{
    // ...
    callback(arg);
    // ...
}

void kill_player_delayed(Player *p, int ticks)
{
    Game::idle(Player::call_kill, static_cast<void *>(p), ticks);
}

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


Альтернативный подход, который, возможно, более идиоматичен и гибок в C++ и включает в себя менее явно написанный код, но имеет более высокие затраты времени выполнения (три косвенных вызова функций и цикл без выделения памяти для кучи на вызов вместо одного косвенного вызова функции). ), состоит в том, чтобы Game::idle принимать объект определенного класса с виртуальным методом обратного вызова. Затем этому классу предоставляется подкласс шаблона, который может вызывать все, что реализует operator(), например результат std::bind.

struct Runnable { virtual ~Runnable(); virtual void invoke() = 0; };

template <typename T> struct TRunnable : Runnable {
    TRunnable(T target) : target(target) {}
    void invoke() { target(); }
private:
    T target;
};
template <typename T> TRunnable<T>* make_Runnable(T obj)
{ return new TRunnable<T>(obj); }

struct Game
{
    static void idle(Runnable *, int);
};

void Game::idle(Runnable *r, int ticks)
{
    // ...
    r->invoke();
    delete r;
    // ...
}

struct Player
{
    // ...
    void kill();
    // ...
};

void kill_player_delayed(Player *p, int ticks)
{
    Game::idle(make_Runnable(std::bind(&Player::kill, p)), ticks);
}

Вы не можете заставить Game::idle принимать результат std::bind напрямую, потому что тип этого объекта не указан (и зависит от того, как вы вызываете std::bind), поэтому его можно использовать только в качестве аргумента для вызова функции template. Вызов виртуального метода для класса адаптера — единственный способ сохранить Game::idle скомпилированный вне очереди и по-прежнему позволять ему использовать объекты связанного вызова.

При любом подходе остерегайтесь проблем со временем жизни объекта. В частности, если Game::idle не вызывает свой обратный вызов перед возвратом, необходимо убедиться, что и исходный объект, и (во втором подходе) объект, возвращенный make_Runnable выжить, пока не сработает обратный вызов. Вот почему make_Runnable использует new.

15.03.2015
  • Я не совсем понимаю ваш первый подход. Что такое функция kill_player_delayed? Я предполагаю, что это относится к тому, что я назвал функцией kill, а kill() в вашем относится к моей смерти()? 16.03.2015
  • @Yaxlat Мои извинения, я неправильно понял ваш исходный пример. Я думал, что вы не показали полную функцию, которая вызывает Game::idle, и обе функции, kill и death, могли быть вызваны через Game::idle. Так что я придумал kill_player_delayed как то, что вызывает Game::idle. 16.03.2015
  • Это действительно хороший подход... настолько хороший, что он уже содержится в стандартной библиотеке под именем std::function! (Изобретать это, возможно, не так уж и хорошо) 16.03.2015
  • @BenVoigt К сожалению, почти весь мой опыт работы с С++ был связан с кодовыми базами с пыльной колодой, которые даже не могут использовать стандартную библиотеку С++ 98, не говоря уже о С++ 11, поэтому у меня есть только смутное представление о том, что все там. Опубликуйте еще один ответ, используя std::function, и я проголосую за него ;-) 16.03.2015

  • 2

    В другом ответе упоминается, что вам нужны два указателя. Однако C++ уже поставляется с контейнерами для этого, поэтому их использование значительно упростит ваш код. (В C++03 некоторые из элементов std:: ниже были std::tr1::).

    Образец кода:

    #include <iostream>
    #include <functional>
    
    struct Game 
    { 
        static void idle( std::function<void()> func, int x )
            { std::cout << "x = " << x << "\n"; func(); }
    };
    
    struct Player
    {
         void death() { std::cout << "player.death\n"; }
         void kill() { Game::idle( std::bind(&Player::death, this), 48 ); }
    };
    
    int main()
    {
        Player p;
        p.kill();
    }
    

    Примечание на всю жизнь: std::bind связывается по значению. Использование *this означает, что копия Player создается и сохраняется в объекте std::function, при необходимости копируется вместе с ним.

    Использование this означает, что объект функции хранит указатель, поэтому, если вы действительно сохраняете объект функции в Game::idle, вы должны позаботиться о том, чтобы этот Player не был уничтожен, прежде чем удалять этот объект функции из списка Game::idle.

    16.03.2015
  • Не могли бы вы немного рассказать о проблемах жизненного цикла при таком подходе? В частности, предположим, что Game::idle помещает свой аргумент в очередь для выполнения позже, намного позже того, как он и Player::kill вернутся. Очевидно, что объект Player должен существовать до тех пор. Безопасно ли рассматривать std::function и неуказанный объект, возвращаемый std::bind, как типы значений, время жизни которых не имеет значения? Есть ли способ заставить std::function удерживать shared_ptr для некоторых или всех аргументов, переданных std::bind? 16.03.2015
  • @zwol отредактировал сообщение, упомянув о жизненных проблемах. Можно использовать shared_ptr с bind, однако это работает только в том случае, если ваш исходный объект также управляется shared_ptr, конечно. 16.03.2015
  • Пример кода с использованием отложенного простоя и shared_ptr . Чтобы обеспечить создание Player под shared_ptr, вы можете иметь частный конструктор и друга Game и дать Game фабричную функцию. 16.03.2015

  • 3

    Поскольку мне действительно не нравится ответ, который приводит void* к другим объектам (почти никогда не требуется в C++!), И никто не опубликовал ответ, используя предложения в комментариях, я собираюсь предложить это.

    Используйте шаблонный тип для обратного вызова!

    Так:

    class Game{
        template<typename Func>
        static void idle(Func &&func, int i){
            // game stuff
            func();
            // other game stuff
        }
    };
    

    Тогда вы не потеряете всю свою безопасность типов (приведение void*), и это должно быть самым быстрым решением.


    Кроме того, когда вы назначаете указатель на функцию, вы можете изменить код, чтобы сделать его более читаемым в этом случае:

    void Player::kill(){
        Game::idle([this](){this->death();}, 48);
    }
    

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

    15.03.2015
  • Это здорово, если обратный вызов не нужно где-то сохранять для последующего использования. Если это так, то std::function<void ()> — лучшее решение. И лямбда-выражения, и результаты std::bind могут быть преобразованы в std::function. 16.03.2015
  • @BenVoigt Я всегда избегаю использования функций virtual или std::function в любом высокопроизводительном программировании (например, в программировании игр) из-за замедления косвенного обращения. Но если бы мне нужно было сохранить обратный вызов, вы абсолютно правы, и изобретать велосипед никому не нужно! 20.03.2015

  • 4

    Вы не можете сделать это просто потому, что указатель на [статическую] функцию является одним указателем sizeof void*. В противоположной функции-члене требуется дополнительная информация, например. два указателя: один для this, а другой для самой функции, поэтому указатель функции-члена имеет sizeof > sizeof(void*).

    Поэтому у вас есть два варианта:

    1. чтобы изменить подпись вашего idle() на этот void idle(void (*)(), void*, int);, чтобы вы могли каким-то образом передать this.
    2. Или создайте статическую переменную, которая будет содержать указатель this. Но это предполагает, что только один death() может находиться в очереди ожидания в любой момент времени.

    1) это то, что люди обычно делают в таких случаях.

    15.03.2015
  • Да, в итоге я сделал 1) благодаря предложению zwol. Я неправильно предположил, что указатели функций С++ включают указатель на объект, которому они принадлежат, когда они являются функциями-членами. 16.03.2015
  • Новые материалы

    Кластеризация: более глубокий взгляд
    Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

    Как написать эффективное резюме
    Предложения по дизайну и макету, чтобы представить себя профессионально Вам не позвонили на собеседование после того, как вы несколько раз подали заявку на работу своей мечты? У вас может..

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

    Как я автоматизирую тестирование с помощью Jest
    Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

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

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..