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

flvtvl50  于 2024-01-09  发布在  其他
关注(0)|答案(1)|浏览(111)

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

  1. #include <vector>
  2. #include <functional>
  3. #include <iostream>
  4. #include <unordered_map>
  5. #include <memory>
  6. enum class GameEvent
  7. {
  8. SomeEvent,
  9. OtherEvent
  10. };
  11. class EventManager
  12. {
  13. public:
  14. template <typename ...Args>
  15. using EventCallback = std::function<void(Args...)>;
  16. template <typename ...Args, typename Callback>
  17. void addSubscriber(const GameEvent& gameEvent, Callback&& callback);
  18. template <typename ...Args>
  19. void notify(const GameEvent& gameEvent, Args... args);
  20. private:
  21. struct ListenerBase
  22. {
  23. virtual ~ListenerBase() = default;
  24. };
  25. template <typename ...Args>
  26. struct ListenerWrapper : public ListenerBase
  27. {
  28. EventCallback<Args...> callback;
  29. template <typename Callback>
  30. ListenerWrapper(Callback&& callback_) : callback(std::forward<Callback>(callback_)) {}
  31. virtual ~ListenerWrapper() = default;
  32. void useCallback(Args... args)
  33. {
  34. callback(args...);
  35. }
  36. };
  37. private:
  38. std::unordered_map<GameEvent, std::vector<std::shared_ptr<ListenerBase>>> eventSubscribers;
  39. };
  40. template<typename ...Args, typename Callback>
  41. inline void EventManager::addSubscriber(const GameEvent& gameEvent, Callback&& callback)
  42. {
  43. eventSubscribers[gameEvent].emplace_back(std::make_shared<ListenerWrapper<Args...>>(std::forward<Callback>(callback)));
  44. }
  45. template<typename ...Args>
  46. inline void EventManager::notify(const GameEvent& gameEvent, Args ...args)
  47. {
  48. auto it = eventSubscribers.find(gameEvent);
  49. if (it == eventSubscribers.end())
  50. return;
  51. for (const auto& callback : it->second)
  52. {
  53. auto castedCallback = std::static_pointer_cast<ListenerWrapper<Args...>>(callback);
  54. castedCallback->useCallback(args...);
  55. }
  56. }
  57. class SomeClass
  58. {
  59. public:
  60. SomeClass(EventManager& em) : em(em) {}
  61. void doSomething()
  62. {
  63. em.notify(GameEvent::SomeEvent, "abc", 6);
  64. }
  65. EventManager& em;
  66. };
  67. class SomeListener
  68. {
  69. public:
  70. SomeListener(EventManager& em)
  71. {
  72. em.addSubscriber<const std::string&, const int&>(GameEvent::SomeEvent,
  73. [this](const std::string& desc, const int& nr){
  74. someEventImpl(desc, nr);
  75. });
  76. }
  77. void someEventImpl(const std::string& desc, const int& nr)
  78. {
  79. std::cout << "Desc: " << desc << " nr: " << nr << std::endl;
  80. }
  81. };
  82. int main() {
  83. EventManager manager;
  84. SomeListener lstr(manager);
  85. SomeClass scs(manager);
  86. scs.doSomething();
  87. return 0;
  88. }

字符串
特别是doSomething()函数中的em.notify(GameEvent::SomeEvent, "abc", 6);行。
所以对于那个普通的“abc”字符串,当我尝试运行程序时,它到达这个事件处理函数,根据Visual Studio编译器,我得到desc为null,它抛出一个异常。
所以我想,我将尝试从Somebody构造函数的lambda函数中删除const&,并从someEventImpl函数中删除,结果是:

  1. SomeListener(EventManager& em)
  2. {
  3. em.addSubscriber<std::string, const int&>(GameEvent::SomeEvent,
  4. [this](std::string desc, const int& nr){
  5. someEventImpl(desc, nr);
  6. });
  7. }
  8. void someEventImpl(std::string desc, const int& nr)
  9. {
  10. std::cout << "Desc: " << desc << " nr: " << nr << std::endl;
  11. }


这给了我完全不同的错误,这是'写访问冲突'(或SIGSEGV根据godbolt编译器)。
我想知道,为什么它在这些情况下都不起作用?为什么这两种实现给予不同的错误?传递一个右值int,如图所示,不会给予任何错误,编译效果很好。
实际上,解决这个问题非常简单。我只需要声明一个std::string并像这样将其传递给eventManager.notify(...)函数:

  1. void doSomething()
  2. {
  3. std::string someString = "abc";
  4. em.notify(GameEvent::SomeEvent, someString, 6);
  5. }


它工作得很好。我只是想知道,为什么这个右值字符串不绑定到const&,为什么整数没有这样的问题,为什么我得到两个不同的错误。提前感谢解释。

pgky5nke

pgky5nke1#

问题是,当你调用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(这违反了你的设计)。

溶液

这是一个你可以做的事情的粗略轮廓:

  1. enum class GameEvent {
  2. SomeEvent,
  3. OtherEvent
  4. };
  5. // define a callback function type for each event
  6. template <GameEvent Event>
  7. using CallbackType =
  8. std::conditional_t<Event == GameEvent::SomeEvent, void(std::string_view, int),
  9. std::conditional_t<Event == GameEvent::OtherEvent, void(int),
  10. void>>;
  11. // helper type
  12. template <GameEvent Event>
  13. struct Callbacks {
  14. using type = CallbackType<Event>;
  15. std::vector<std::function<type>> callbacks;
  16. };

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

用法

您可以使用这样的EventManager

  1. EventManager e;
  2. e.addSubscriber<GameEvent::SomeEvent>([](std::string_view s, int x) {
  3. // ...
  4. });
  5. e.notify<GameEvent::SomeEvent>("foo", 123);

进一步说明

理想情况下,您还可以使用std::invocable等方法为addSubscribernotify添加适当的约束。否则,如果您混淆了任何涉及的类型,您将在emplace_back或调用std::function::operator()()时获得编译器错误。

展开查看全部

相关问题