【问题标题】:Alternative to a function pointer with reference capture替代具有引用捕获的函数指针
【发布时间】:2015-04-19 18:53:23
【问题描述】:

我正在编写一个事件处理程序来监听按键,然后在任何按下的键上调用一个处理程序。我的目标是允许这样的事情:

Entity player(0, 0);

EventHandler eh([&](char c) {
    switch (c) {
        case 'W': {
            player.moveBy(0,-1);
            break;
        }
        case 'S': {
            player.moveBy(0, 1);
            break;
        }
        case 'A': {
            player.moveBy(-1, 0);
            break;
        }
        case 'D': {
            player.moveBy(1, 0);
            break;
        }
    }
});

Entity 只是一个可移动的点状对象。

我已经准备好了,然后我意识到不能将带有引用捕获的 lambdas 制作成函数指针(回想起来,原因是有道理的)。

我能找到的唯一替代方法是使用std::/boost::function,但语法相当丑陋,而且显然它们带来了相当大的开销。

这个系统有什么好的替代品?我希望能够将某种“处理程序”传递给EventHandler,它接受一个字符,并且能够对某些外部范围执行副作用。

在下面的源代码中,LockedQueue 是一个 FIFO 队列,使用 mutexes 使其成为线程安全的。

EventHandler.h:

#ifndef EVENT_HANDLER_H
#define EVENT_HANDLER_H

#include <vector>
#include <atomic>

#include "LockedQueue.h"

class EventHandler {

    typedef void(*KeyHandler)(char);

    std::atomic<bool> listenOnKeys = false;

    std::vector<char> keysToCheck;

    LockedQueue<char> pressedKeys;

    KeyHandler keyHandler = nullptr;

    void updatePressedKeys();

    void actOnPressedKeys();

public:
    EventHandler();
    EventHandler(KeyHandler);

    ~EventHandler();

    void setKeyHandler(KeyHandler);

    void setKeysToListenOn(std::vector<char>);

    void listenForPresses(int loopMSDelay = 100);
    void stopListening();

};

#endif

EventHandler.cpp:

#include "EventHandler.h"

#include <windows.h>
#include <WinUser.h>
#include <thread>
#include <stdexcept>

EventHandler::EventHandler() {

}

EventHandler::EventHandler(KeyHandler handler) {
    keyHandler = handler;
}

EventHandler::~EventHandler() {
    stopListening();
}

void EventHandler::updatePressedKeys() {
    for (char key : keysToCheck) {
        if (GetAsyncKeyState(key)) {
            pressedKeys.push(key);
        }
    }
}

void EventHandler::actOnPressedKeys() {
    while (!pressedKeys.empty()) {
        //Blocking if the queue is empty
        //We're making sure ahead of time though that it's not
        keyHandler(pressedKeys.waitThenPop());
    }
}

void EventHandler::setKeyHandler(KeyHandler handler) {
    keyHandler = handler;
}

void EventHandler::setKeysToListenOn(std::vector<char> newListenKeys) {
    if (listenOnKeys) {
        throw std::runtime_error::runtime_error(
            "Cannot change the listened-on keys while listening"
        );
        //This could be changed to killing the thread by setting
        // listenOnKeys to false, changing the keys, then restarting
        // the listening thread. I can't see that being necessary though.
    }

    //To-Do:
    //Make sure all the keys are in upper-case so they're
    // compatible with GetAsyncKeyState

    keysToCheck = newListenKeys;

}

void EventHandler::listenForPresses(int loopMSDelay) {
    listenOnKeys = true;
    std::thread t([&]{
        do {
            updatePressedKeys();
            actOnPressedKeys();
            std::this_thread::sleep_for(std::chrono::milliseconds(loopMSDelay));
        } while (listenOnKeys);
    });
    t.join();
}

void EventHandler::stopListening() {
    listenOnKeys = false;
}

编辑:

哎呀。请注意,listenForPresses 已“损坏”,因为我正在加入函数内部,因此控制权永远不会离开它。我需要想出一个解决方法。虽然不会改变问题,但代码在当前状态下不可测试。

【问题讨论】:

    标签: c++ lambda


    【解决方案1】:

    我能找到的唯一替代方法是使用 std::/boost::function,但语法相当丑陋,而且显然它们带来了相当大的开销。

    与可内联函数相比,开销是不错的,但it's measured in nanoseconds。如果您每秒只调用该函数 60 次,那么开销是无法估量的。

    也就是说,如果您需要能够随时更改事件处理程序,您唯一的选择是虚拟方法调用,其开销相似。本文深入探讨了这些选择对性能的影响:Member Function Pointers and the Fastest Possible C++ Delegates。

    如果您愿意将 EventHandler 对象限制为执行在编译时定义的单个代码块,请使用模板来存储编译器为 lambda 生成的类型的实例;这应该允许编译器执行更多优化,因为它可以确定正在调用什么代码。在这种情况下,KeyHandler 成为模板类型,而 lambda 的类型可以通过 decltype 关键字找到:

    template <class KeyHandler>
    class EventHandler {
        // elided
    }
    
    void EventLoopDecltype() {
        Entity player(0, 0);
        auto myEventHandler = [&](char ch) { /* elided */ };
        EventHandler<decltype(myEventHandler)> eh(myEventHandler);
    }
    

    或者(对于调用者来说更方便)推断为模板函数的参数:

    template <class KeyHandler>
    EventHandler<KeyHandler> MakeEventHandler(KeyHandler handler) {
        return EventHandler<KeyHandler>(handler);
    }
    
    void EventLoopInferred() {
        Entity player(0, 0);
    
        auto eh = MakeEventHandler([&](char c) {
            // elided
        });
    }
    

    【讨论】:

    • 谢谢。我只需要一段静态代码;在运行时我不需要做任何关键的“重新绑定”。不过,我不明白您在谈论什么模板。我知道模板是用于通用编程的东西;来表示一个类型。这如何帮助我“插入”一段代码?这种技术有具体的名称吗?
    • 我已经重读了你的帖子几次,但我仍然不明白你的最后一段。请给我一段极简的代码来演示一下吗?
    • 使用模板类演示了这两种方法。你当然可以使用一个函数对象,它需要更少的魔法(它是一个你自己用operator()定义的类),但你必须手动捕获你需要的值。
    • 你们都说std::function 的开销可以忽略不计,所以我想我会试试。如果由于某种原因它不起作用,我将尝试您的其他方法之一。谢谢。
    【解决方案2】:

    std::function 和 boost::function 不会带来任何远程意义的开销,考虑到在这种情况下您对它们的使用有多轻。在确定所谓的缺点确实适用于您之前,您放弃了解决方案,从而犯了严重错误。

    您当然也可以使用另一个答案中描述的模板,但实际上没有必要。

    【讨论】:

    • 希望看到这样做还有多少工作量会阻止你这样做,除非你真的知道你需要它!
    猜你喜欢
    • 2013-10-03
    • 1970-01-01
    • 2015-11-11
    • 2015-08-25
    • 2020-03-05
    • 2018-12-08
    • 2015-10-15
    • 2021-01-29
    • 2017-05-30
    相关资源
    最近更新 更多