右值 std::string 未正确绑定到 const std::string& 或返回 SIGSEGV

mat*_*ush 2 c++ string templates type-deduction

因此,当我为游戏编写一些功能时,我遇到了一些我不理解的问题,我寻求解释。因此,我实现了一个简单的事件系统,允许我在代码中向不同的系统发送通信。一切工作正常,直到我遇到将右值 std::string 传递给我的函数之一的问题,请考虑以下代码:

#include <vector>
#include <functional>
#include <iostream>
#include <unordered_map>
#include <memory>

enum class GameEvent
{
    SomeEvent,
    OtherEvent
};
class EventManager
{
public:
    template <typename ...Args>
    using EventCallback = std::function<void(Args...)>;

    template <typename ...Args, typename Callback>
    void addSubscriber(const GameEvent& gameEvent, Callback&& callback);

    template <typename ...Args>
    void notify(const GameEvent& gameEvent, Args... args);
private:
    struct ListenerBase
    {
        virtual ~ListenerBase() = default;
    };

    template <typename ...Args>
    struct ListenerWrapper : public ListenerBase
    {
        EventCallback<Args...> callback;

        template <typename Callback>
        ListenerWrapper(Callback&& callback_) : callback(std::forward<Callback>(callback_)) {}
        virtual ~ListenerWrapper() = default;

        void useCallback(Args... args)
        {
            callback(args...);
        }
    };
private:
    std::unordered_map<GameEvent, std::vector<std::shared_ptr<ListenerBase>>> eventSubscribers;
};



template<typename ...Args, typename Callback>
inline void EventManager::addSubscriber(const GameEvent& gameEvent, Callback&& callback)
{
    eventSubscribers[gameEvent].emplace_back(std::make_shared<ListenerWrapper<Args...>>(std::forward<Callback>(callback)));
}

template<typename ...Args>
inline void EventManager::notify(const GameEvent& gameEvent, Args ...args)
{
    auto it = eventSubscribers.find(gameEvent);

    if (it == eventSubscribers.end())
        return;

    for (const auto& callback : it->second)
    {
        auto castedCallback = std::static_pointer_cast<ListenerWrapper<Args...>>(callback);
        castedCallback->useCallback(args...);
    }
}

class SomeClass
{
    public:
    SomeClass(EventManager& em) : em(em) {}

    void doSomething()
    {
        em.notify(GameEvent::SomeEvent, "abc", 6);
    }

    EventManager& em;
};

class SomeListener
{
    public:
    SomeListener(EventManager& em)
    {
        em.addSubscriber<const std::string&, const int&>(GameEvent::SomeEvent,
        [this](const std::string& desc, const int& nr){
            someEventImpl(desc, nr);
        });
    }

    void someEventImpl(const std::string& desc, const int& nr)
    {
        std::cout << "Desc: " << desc << " nr: " << nr << std::endl;
    }
};


int main() {

   EventManager manager;
   SomeListener lstr(manager);
   SomeClass scs(manager);

   scs.doSomething();
   

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

em.notify(GameEvent::SomeEvent, "abc", 6);特别是 doSomething() 函数中的这一行:

因此,使用这个简单的“abc”字符串,当我尝试运行程序并且它到达此事件处理函数时, desc根据 Visual Studio 编译器,我得到的是 null,并且它抛出异常。

const&所以我想,我将尝试从 SomeListener 构造函数中的 lambda 函数中删除,并从someEventImpl 函数中删除,结果如下:

SomeListener(EventManager& em)
    {
        em.addSubscriber<std::string, const int&>(GameEvent::SomeEvent,
        [this](std::string desc, const int& nr){
            someEventImpl(desc, nr);
        });
    }

    void someEventImpl(std::string desc, const int& nr)
    {
        std::cout << "Desc: " << desc << " nr: " << nr << std::endl;
    }
Run Code Online (Sandbox Code Playgroud)

这给了我完全不同的错误,即“写访问冲突”(或根据 godbolt 编译器的 SIGSEGV)。

我想知道,为什么它在这些情况下不起作用?为什么这两种实现都会给出不同的错误?如图所示传递右值 int 不会给出任何错误并且编译良好。

事实上,解决这个问题非常简单。我只需声明一个 std::string 并将其传递给函数,eventManager.notify(...)如下所示:

void doSomething()
    {
        std::string someString = "abc";
        em.notify(GameEvent::SomeEvent, someString, 6);
    }
Run Code Online (Sandbox Code Playgroud)

而且效果很好。我只是想知道,为什么这个右值字符串不绑定到 const&,为什么整数没有这样的问题以及为什么我收到两个不同的错误。预先感谢您的解释。

Jan*_*tke 6

问题是当你打电话时em.notify(GameEvent::SomeEvent, "abc", 6)Args推导出来[const char*, int]

  • 存储的类型是ListenerWrapper<std::string const&, int const&>
  • 被调用的类型是ListenerWrapper<Args...> = ListenerWrapper<const char*, int>

这是未定义的行为,UBSan 捕获了这一点: https: //godbolt.org/z/WK4qWj3e1

根本问题

你的设计非常脆弱。当您 时std::static_pointer_cast,您需要向下转换为您最初存储的类型。然而,你们没有采取任何措施来确保这一点。在目前的状态下,您的设计并不比std::any前后投射更好。你的多态性ListenerBase完全没用。

您需要以某种方式保证每种类型的事件都有一个特定的、硬编码的回调签名。隐式转换也应该有效,例如const char* -> std::string(这会破坏您的设计)。

解决方案

这是您可以做的事情的粗略轮廓:

enum class GameEvent {
    SomeEvent,
    OtherEvent
};

// define a callback function type for each event
template <GameEvent Event>
using CallbackType =
  std::conditional_t<Event == GameEvent::SomeEvent, void(std::string_view, int),
  std::conditional_t<Event == GameEvent::OtherEvent, void(int),
  void>>;

// helper type
template <GameEvent Event>
struct Callbacks {
    using type = CallbackType<Event>;
    std::vector<std::function<type>> callbacks;
};
Run Code Online (Sandbox Code Playgroud)
struct EventManager : Callbacks<GameEvent::SomeEvent>, Callbacks<GameEvent::OtherEvent> {

    template <GameEvent Event, typename F>
    void addSubscriber(F&& callback) {
        Callbacks<Event>::callbacks.emplace_back(std::forward<F>(callback));
    }

    template <GameEvent Event, typename... Args>
    void notify(Args... args) {
        for (auto& f : Callbacks<Event>::callbacks) {
            f(args...);
        }
    }
};
Run Code Online (Sandbox Code Playgroud)

这个解决方案要好得多,因为它是类型安全的。在这种情况下,GameEvent总是需要在编译时知道。公平地说,您基本上总是知道要向哪个游戏事件添加订阅者,或者要通知哪些事件,所以这并不是真正的问题。

用法

你可以使用这样EventManager的:

struct EventManager : Callbacks<GameEvent::SomeEvent>, Callbacks<GameEvent::OtherEvent> {

    template <GameEvent Event, typename F>
    void addSubscriber(F&& callback) {
        Callbacks<Event>::callbacks.emplace_back(std::forward<F>(callback));
    }

    template <GameEvent Event, typename... Args>
    void notify(Args... args) {
        for (auto& f : Callbacks<Event>::callbacks) {
            f(args...);
        }
    }
};
Run Code Online (Sandbox Code Playgroud)

请参阅编译器资源管理器中的实时示例

进一步说明

理想情况下,您还可以使用 et al 添加适当的约束std::invocable。到addSubscribernotify。否则,如果您弄乱了涉及的任何类型,您将emplace_back在调用std::function::operator()().