英文:
How to handle template class static members and storage in situations where leaving to static cleanup isn't desired
问题
我最近重新开始学习C++,经历了多年的C#和Java开发之后,我非常喜欢C++在我离开后的发展(自C++11以来,现在我正在学习C++20!)。模板的强大让我感到非常兴奋。然而,我很快遇到了一个似乎无法解决的问题。
我编写了一个使用模板驱动的事件系统,我非常满意。
struct imgui_event_listener
{
void onCreatedWindow(events::ImGui_CreatedWindow f)
{
}
}
dispatcher::connect(&listener, &imgui_event_listener::onCreatedWindow);
...
dispatcher::fire(events::ImGui_CreatedWindow{ window });
我很喜欢它,它不需要模板参数,可以使用任意的结构体来表示事件。然而,有一个大问题,那就是我的事件系统几乎无限且完全不可知的潜在不同版本,都使用一些unordered_map,这些map需要被存储在某个地方。它们目前是静态的,存在于方法内部。
类似于...
template <typename ListenerType, typename Type>
using FPtr = void(ListenerType::*)(Type);
template <typename ListenerType, typename Type>
using Tuple = eastl::tuple<ListenerType*, FPtr<ListenerType, Type>>;
template <typename ListenerType, typename Type>
using ListenerMap = eastl::unordered_map<uint32_t, Tuple<ListenerType, Type>>;
class EventSystem
{
template <typename ListenerType, typename Type>
ListenerMap<ListenerType, Type>& listenerMap()
{
static ListenerMap<ListenerType, Type> listenerMap;
return listenerMap;
}
}
我现在是一个相对新手,所以如果有什么奇怪的地方,请原谅。
这似乎太方便和令人满意,以至于我以为人们会因某种原因而受到指责,但事实上这似乎是必要的时候通常的做法。
但是我正在使用的游戏引擎有一个内存管理器,不使用或建议使用STL,而是有自己的替代品,它使用了EA开发的用于游戏的开源内存分配器,以便跟踪内存。它会跟踪所有内存使用情况,并在静态对象被移除时报告任何内存泄漏。
不幸的是,尽管这些容器是静态的,但这些unordered_map的节点是从内存管理堆中分配的。预期的用法是在清理时清空它们,这是其他地方的做法。
现在,我怀疑实际上,在内存管理器已经发出警告后,这些静态实例的map将自动被处理,然而,即使只在调试时也无法避免它投诉,退出时出现异常并看到内存地址被报告为泄漏是很烦人的,当它发生时,我感到羞愧!这也可能掩盖了真正的问题。我确信我可以使用普通的STL map来强制分配内存,但我真的想遵循引擎开发人员所制定的道路,避免未来的麻烦。因此,这更多地是关于方便和虚荣,而不是其他任何东西,但我需要一种方法来解决这个问题,而不牺牲我的事件系统的优雅用法。
这引导我进入了模板元编程的世界,试图在事件连接形成和模板实例在编译时创建时将所有这些模板参数汇总到一个巨大的元组中,以便能够提前手动清理。但似乎模板元编程需要为每个指令创建新的“变量名称”作为typedef或using的方式,这意味着我不能逐步执行这个操作,但说实话,这个领域如此复杂,相关示例稀缺,我不知道是否遗漏了什么。
我尝试考虑这些可以存储在哪里,但我真的无法想象会有什么不同的方式。
我也可以手动维护所有我的事件的列表,但那么我将失去其中一个更酷更灵活的功能。
或者我可以在其他地方存储它们,以便能够访问它们。这似乎是一个进退两难的情况,因为它们的存在要求它们在某个无法访问的并行宇宙中。
所以是的,我知道有办法可以解决这个问题,但它们要么牺牲了系统的某个特性,要么可能会引发未来的问题。我不确定我是否曾经在这里提问过问题,但这个问题确实让我困扰,我真的找不到类似问题的解决方案,大多数人乐于使用std库,没有内存管理器,将静态实例放在函数内部是一个非常令人满意的简单方式。谢谢你的时间!请原谅如果有点啰嗦,但我觉得我需要提供一些背景。我知道没有太多的代码可供展示,但希望你明白我的意思!
英文:
I've recently gotten back into C++ after many years in C# and java land, and am loving where C++ has gone in my absence (since pre C++11, am now soaking up C++20!). The power of templates is really exciting to me. However, I quickly ran into something that seems impossible to deal with.
I've written a template driven event system which am really happy with.
struct imgui_event_listener
{
void onCreatedWindow(events::ImGui_CreatedWindow f)
{
}
}
dispatcher::connect(&listener, &imgui_event_listener::onCreatedWindow);
...
dispatcher::fire(events::ImGui_CreatedWindow{ window });
I love it, it requires no template parameters, can use arbitary structs for events. There's one big snag though, and that is the near infinite and completely unknowable potential invisible versions of my event system, all using a few unordered_maps that need to go somewhere. They are currently static within methods.
Stuff like...
template <typename ListenerType, typename Type>
using FPtr = void(ListenerType::*)(Type);
template<typename ListenerType, typename Type>
using Tuple = eastl::tuple<ListenerType*, FPtr<ListenerType, Type>>;
template <typename ListenerType, typename Type>
using ListenerMap = eastl::unordered_map<uint32_t, Tuple<ListenerType, Type>>;
class EventSystem
{
template<typename ListenerType, typename Type>
ListenerMap<ListenerType, Type>& listenerMap()
{
static ListenerMap<ListenerType, Type> listenerMap;
return listenerMap;
}
}
I'm a relative noob these days so forgive if anything's weird.
This SEEMS to be too convenient and satisfying to be recommended, I expected people to be shouted down for some reason but it seems this is the way it's by necessity done most of the time.
But the game engine I'm working with has a memory manager, and doesn't use or recommend the STL, having its own replacement that's using the one open-sourced by EA for games, and has their own allocator that is to be used for it so it can track memory.
This tracks all memory usage and reports with an exception any leaks that occur at the time static objects are being removed.
Unfortunately despite the containers being static, the nodes of those unordered_maps are malloced from the memory managed heap. Expected usage would be to clear them on clean up which is what happens everywhere else.
Now, I suspect in actuality, these maps static instances will automatically be dealt with after the memory manager has already complained, however there's really no real way around avoiding it complain, even if only in debug, an exception on quit and seeing memory addresses reported as leaks is annoying, and it makes me feel ashamed when it does! It also could obscure real issues. I'm sure I could brute force normal STL maps with allocations, but I really want to follow the road laid out by the developers of the engine and avoid any future headaches. So its more about convenience and vanity than anything else but I need a way around this without sacrificing my event system's elegance of usage.
This lead me into the world of template meta-programming to try and collate all these template parameters into some mega tuple as the event connections are formed and the template instances are created at compile time, and looking into every conceivable pattern where I'd somehow be able to unwind all the template instances I've created to get at these in advance and be able to manually clean up, but it seems that template meta-programming requiring new 'variable names' for each instruction as typedefs or using means I can't iteratively do this from what I can see, but honestly the field is so complicated and light on relevant examples I don't know if I'm missing anything.
I've tried to think about where else these could be stored, but I just can't fathom how it'd be any different.
I could also just manually maintain a list of all my events, but then I'd lose one of its cooler and more flexible features.
OR somewhere else I can store them where I'd be able to access them. It seems like a catch-22 situation where their very existence demands they be in some inaccessible parallel universe..
So yeah, I know there are ways I could solve this without asking for help but they are all either sacrificing a feature of the system or potentially causing future issues. Not sure I've ever asked a question here, but this has really stumped me and I can't really find a similar issue from someone else, most are happy to use std library, have no memory manager, and putting static instances within functions is a super satisfyingly simple way to do it.
Thanks for your time! Forgive if a bit rambly but felt I needed to provide context. I know there's not really much code to show but you get my point I hope!
答案1
得分: 0
以下是翻译的内容:
可能是这样的一些代码:
std::vector<std::function<void()>> cleanup_registry;
class EventSystem
{
template<typename ListenerType, typename Type>
ListenerMap<ListenerType, Type>& listenerMap()
{
static ListenerMap<ListenerType, Type> listenerMap;
static int dummy = (
cleanup_registry.push_back([&]() { listenerMap.clear(); }),
0);
return listenerMap;
}
};
void cleanup() {
for (auto& cleaner : cleanup_registry) {
cleaner();
}
std::vector<std::function<void()>>{}.swap(cleanup_registry);
}
在游戏引擎终止之前调用 `cleanup()`。
---
您可以使用类似的类型擦除技术,从您的映射中消除 `ListenerType`,只需为每种事件类型保留一个映射。类似于以下代码:
template <typename Event>
using ListenerMap = eastl::unordered_map<uint32_t, std::function<void(Event)>>;
然后 `dispatcher::connect`(其中实际的监听器类型可用)将制造一个回调 lambda,然后将其放入映射中。
英文:
Something along these lines perhaps:
std::vector<std::function<void()>> cleanup_registry;
class EventSystem
{
template<typename ListenerType, typename Type>
ListenerMap<ListenerType, Type>& listenerMap()
{
static ListenerMap<ListenerType, Type> listenerMap;
static int dummy = (
cleanup_registry.push_back([&]() { listenerMap.clear(); }),
0);
return listenerMap;
}
};
void cleanup() {
for (auto& cleaner : cleanup_registry) {
cleaner();
}
std::vector<std::function<void()>>{}.swap(cleanup_registry);
}
Call cleanup()
right before your game engine terminates.
You can use similar type-erasure techniques to eliminate ListenerType
from your maps, having just one map per event type. Something like
template <typename Event>
using ListenerMap = eastl::unordered_map<uint32_t, std::function<void(Event)>>;
Then dispatcher::connect
(where actual listener type is available) would manufacture a callback lambda to be placed into the map.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论