/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ // Original file Copyright Crytek GMBH or its affiliates, used under license. // Description : A simple, intelligent and efficient container for listeners. // This is designed to provide a simple & consistent interface and behavior // for adding, removing and iterating listeners - hopefully avoiding the // common pitfalls such as duplicated elements, invalid iterators and // dangling pointers. #ifndef CRYINCLUDE_CRYCOMMON_CRYLISTENERSET_H #define CRYINCLUDE_CRYCOMMON_CRYLISTENERSET_H #pragma once #include /************************************************************************ Core elements: * CListenerSet - The collection of listeners. * CListenerSet::Notifier - The iterator for safely calling listeners in sequence. [Where T is a pointer or something with pointer traits] Advantages: * Greatly reduces the complexity of managing listener collections. * Can safely add and remove listeners during listener iteration. * Automatically and safely removes NULL elements. * Simple interface (but different from an STL collection to avoid confusion). * Checks to see all listeners have been removed at destruction. * Low overhead implementation (cheap use of std::vector and minimal heap allocation). * Can add debug checks as needed - including stack tracing of Add() calls. * Safe for recursive notification chains. * Works with vanilla pointers and smart pointers. * Provides full support for named listeners to aid debugging. * Listener names tracked during notification to help resolve crashes. * Designed to ensure names are recorded correctly in crash dump files. Named listener support: Supplying a name for a listener provides valuable debug information in the following cases: * Resolving crashes during listener notification - the name will help trace what listener caused the crash. * Resolving which listeners are present in the CListenerSet during runtime. IMPORTANT: Please ensure heap allocated strings passed in as names are marked as such ie. m_listeners.Add(pListener, "MyListener"); // OK: Static string passed. m_listeners.Add(pListener2, m_myName.c_str(), false); // OK: Heap string passed and marked as non-static. m_listeners.Add(pListener3, m_myName.c_str(), true); // BAD: Heap string passed and marked as static (potential CRASH). Why store names like this? * 99% of use cases use static strings - so why allocate memory to make copies of static data? * Pointers to static strings will *always* survive crash dumps - great for debugging crashes. * We can enable debug support in QA builds - key for catching rare listener related crashes. Example: class CMyWorld { public: void AddListener(IMyWorldListener* pListener, const char* szName) { m_listeners.Add(pListener, szName); } void RemoveListener(IMyWorldListener* pListener) { m_listeners.Remove(pListener); } void NotifyListeners(CSomeEvent& event) { for (TWorldListeners::Notifier notifier(m_listeners); notifier.IsValid(); notifier.Next()) { notifier->OnWorldEvent(event); } } private: typedef CListenerSet TWorldListeners; TWorldListeners m_listeners; }; // Implements IMyWorldListener CMyWorldUser::OnWorldEvent(CSomeEvent& event) { // OK: Removing a listeners within an event handler m_pWorld->RemoveListener(this); // OK: Notifying listeners within an event handler m_pWorld->NotifyListeners(CSomeEvent newEvent(USER_REMOVED, this)); } *************************************************************************/ #ifndef _RELEASE #define CRY_LISTENERSET_DEBUG #endif // Forward decl. template class CListenerNotifier; // Main listener collection class used in conjunction with CListenerNotifier. template class CListenerSet { public: // NOTE: No default constructor in favor of forcing users to provide an expectedCapacity inline CListenerSet(size_t expectedCapacity); inline /*non-virtual*/ ~CListenerSet(); // Appends a listener to the end of the collection. Name is optional but recommended. inline bool Add(T pListener, const char* name = NULL, bool staticName = true); // Removes a listener from the collection. inline void Remove(T pListener); // Removes all listeners from the collection (NOTE: prefer informing listeners to remove themselves) inline void Clear(bool bFreeMemory = false); // Returns true if this contains pListener inline bool Contains(T pListener) const; // Returns number of valid listeners inline size_t ValidListenerCount() const; // Returns true if no valid listeners exist inline bool Empty() const; // Reserves space to help avoid runtime reallocation inline void Reserve(size_t capacity); // Returns the memory size of this object (to support CrySizer) inline size_t MemSize() const; // Returns true if currently in the process of notifying listeners. inline bool IsNotifying() const; // Allow access for Notifier for iteration friend class CListenerNotifier; // Allow TListeners::Notifier style usage typedef class CListenerNotifier Notifier; void GetMemoryUsage(ICrySizer* pSizer) const { pSizer->AddContainer(m_listeners); #if defined(CRY_LISTENERSET_DEBUG) pSizer->AddContainer(m_allocatedNames); #endif } private: // DO NOT REMOVE - following methods only to be accessed only via CNotifier struct ListenerRecord { ListenerRecord() : m_pListener() {} ListenerRecord(T pListener, const char* szName = NULL) : m_pListener(pListener) #ifdef CRY_LISTENERSET_DEBUG , m_szName(szName) #endif {} bool operator==(const ListenerRecord& other) const { return m_pListener == other.m_pListener; } bool operator==(const T& other) const { return m_pListener == other; } T m_pListener; // The listener reference #ifdef CRY_LISTENERSET_DEBUG const char* m_szName; // Name of tracked listener (owned if pointing to data in m_allocatedNames) #endif }; typedef std::vector TListenerVec; typedef std::vector TAllocatedNameVec; inline void StartNotificationScope(); inline void EndNotificationScope(); inline void EraseNullElements(); private: TListenerVec m_listeners; // Collection of unique listeners. size_t m_activeNotifications; // Counts current notifications in progress (cleanup cannot occur unless this is 0). bool m_cleanupRequired; // Indicates NULL elements in listener. bool m_freeMemOnCleanup; // Indicates how to clean up. #ifdef CRY_LISTENERSET_DEBUG // Used to delete heap allocated names inline void DeleteName(const char* name); TAllocatedNameVec m_allocatedNames; // Collection of strings pointing at heap allocated (i.e. copied) names (typically empty) #endif }; // Helper class used to iterate listeners during listener notification. template class CListenerNotifier { public: ILINE CListenerNotifier(CListenerSet& listeners); ILINE /*non-virtual*/ ~CListenerNotifier(); // True if the current element is ready for iteration ILINE bool IsValid(); // Dereference current listener, MUST only be done after a call to IsValid(). ILINE T operator->(); // Dereference current listener, MUST only be done after a call to IsValid(). ILINE T operator*(); // Move to next valid listener (skipping NULL elements) ILINE void Next(); // Returns the name of the listener (if available) inline const char* Name() const; private: CListenerSet& m_listenerSet; // ListenerSet being notified T m_pListener; // Current listener at index (resolved by IsValid(), cleared after each dereference) size_t m_index; // Current index of element (incremented by next) #ifdef CRY_LISTENERSET_DEBUG const char* m_szName; // Name of the listener (if provided) to aid debugging #endif }; /******************************************************************************************/ template inline CListenerSet::CListenerSet(size_t expectedCapacity) : m_activeNotifications(0) , m_cleanupRequired(false) , m_freeMemOnCleanup(false) { // Reserve the expected capacity to avoid reallocations m_listeners.reserve(expectedCapacity); } template inline CListenerSet::~CListenerSet() { // Ensure no notifications are in progress CRY_ASSERT(m_activeNotifications == 0); // Ensure NULL elements were removed at end of last notification CRY_ASSERT(!m_cleanupRequired); } // Appends a listener to the end of the collection. Name is optional but recommended. template inline bool CListenerSet::Add(T pListener, const char* name, bool staticName) { bool success = false; // Ensure the listener exists CRY_ASSERT(pListener); if (pListener) { // Ensure the listener is only added once if (!Contains(pListener)) { // Resolve name buffer safe for usage outside of this scope const char* safeName = name; #ifdef CRY_LISTENERSET_DEBUG // If a name was provided but it's not static data if (name && !staticName) { // Add it to the list of heap allocated names (that we need to later delete) m_allocatedNames.push_back(name); safeName = m_allocatedNames.back().c_str(); } #endif m_listeners.push_back(ListenerRecord(pListener, safeName)); success = true; } } return success; } // Removes a listener from the collection. template inline void CListenerSet::Remove(T pListener) { typename TListenerVec::iterator endIter(m_listeners.end()); typename TListenerVec::iterator iter(std::find(m_listeners.begin(), endIter, pListener)); if (iter != endIter) { #ifdef CRY_LISTENERSET_DEBUG // Delete name if it was heap allocated if (const char* name = iter->m_szName) { DeleteName(name); } #endif // If no notifications in progress if (m_activeNotifications == 0) { // Just delete the listener entry immediately m_listeners.erase(iter); } else // Notification(s) in progress, cannot re-order listeners { // Mark for cleanup iter->m_pListener = NULL; m_cleanupRequired = true; m_freeMemOnCleanup = false; } } else // The listener is not in the set { // TODO: Warn about redundant Remove() } } // Removes all listeners from the collection (NOTE: prefer informing listeners to remove themselves) template inline void CListenerSet::Clear(bool bFreeMemory) { // If no notifications in progress if (m_activeNotifications == 0) { // Simply clear the listeners immediately if (bFreeMemory) { stl::free_container(m_listeners); } else { m_listeners.clear(); } } else { // Mark all listeners for cleanup std::fill(m_listeners.begin(), m_listeners.end(), ListenerRecord()); m_cleanupRequired = true; m_freeMemOnCleanup = true; } #ifdef CRY_LISTENERSET_DEBUG // Safe to clear allocated names immediately (no references exist any more) if (bFreeMemory) { stl::free_container(m_allocatedNames); } else { m_allocatedNames.clear(); } #endif } // Returns true if this contains pListener template inline bool CListenerSet::Contains(T pListener) const { return stl::find(m_listeners, pListener); } // Returns number of valid listeners template inline size_t CListenerSet::ValidListenerCount() const { size_t validCount = m_listeners.size(); if (m_cleanupRequired) { // Remove the count of NULL elements from the result validCount = validCount - std::count(m_listeners.begin(), m_listeners.end(), T()); } return validCount; } // Returns true if no valid listeners exist template inline bool CListenerSet::Empty() const { return ValidListenerCount() == 0; } // Reserves space to help avoid runtime reallocation template inline void CListenerSet::Reserve(size_t capacity) { m_listeners.reserve(capacity); } // Returns the memory size of this object (to support CrySizer) template inline size_t CListenerSet::MemSize() const { size_t size = sizeof(CListenerSet) + sizeof(ListenerRecord) * m_listeners.size(); #ifdef CRY_LISTENERSET_DEBUG size += sizeof(typename TAllocatedNameVec::value_type); for (typename TAllocatedNameVec::const_iterator iter(m_allocatedNames.begin()); iter != m_allocatedNames.end(); ++iter) { size += iter->GetAllocatedMemory(); } #endif return size; } template inline bool CListenerSet::IsNotifying() const { return m_activeNotifications > 0; } template inline void CListenerSet::StartNotificationScope() { ++m_activeNotifications; } template inline void CListenerSet::EndNotificationScope() { // Ensure at least one notification scope was started CRY_ASSERT(m_activeNotifications > 0); // If this is the last notification if (--m_activeNotifications == 0) { EraseNullElements(); } } template inline void CListenerSet::EraseNullElements() { // Ensure no modification while notification(s) are ongoing CRY_ASSERT(m_activeNotifications == 0); if (m_cleanupRequired && m_activeNotifications == 0) { stl::find_and_erase_all(m_listeners, T()); if (m_freeMemOnCleanup && m_listeners.empty()) { stl::free_container(m_listeners); } m_cleanupRequired = false; m_freeMemOnCleanup = false; } } #ifdef CRY_LISTENERSET_DEBUG // Used to delete heap allocated names template inline void CListenerSet::DeleteName(const char* name) { if (!m_allocatedNames.empty()) { typename TAllocatedNameVec::iterator endIter(m_allocatedNames.end()); for (typename TAllocatedNameVec::iterator iter(m_allocatedNames.begin()); iter != endIter; ++iter) { // Is this the source string? if (iter->c_str() == name) { // Delete it m_allocatedNames.erase(iter); break; } } } } #endif // defined CRY_LISTENERSET_DEBUG /******************************************************************************************/ template ILINE CListenerNotifier::CListenerNotifier(CListenerSet& listeners) : m_listenerSet(listeners) , m_pListener() , m_index(0) #ifdef CRY_LISTENERSET_DEBUG , m_szName() #endif { // Flag iteration to listener set to ensure no erase is attempted during iteration m_listenerSet.StartNotificationScope(); // If first element is NULL, move to next valid element if (!IsValid()) { Next(); } } template ILINE CListenerNotifier::~CListenerNotifier() { // Erases any NULL elements from listeners m_listenerSet.EndNotificationScope(); } // True if the current element is ready for iteration template ILINE bool CListenerNotifier::IsValid() { if (!m_pListener) { // Always check with original collection if (m_index < m_listenerSet.m_listeners.size()) { const typename CListenerSet::ListenerRecord & record(m_listenerSet.m_listeners[m_index]); m_pListener = record.m_pListener; #ifdef CRY_LISTENERSET_DEBUG m_szName = record.m_szName; #endif } } return m_pListener != NULL; } // Dereference current listener, MUST only be done after a call to IsReady(). template ILINE T CListenerNotifier::operator->() { return operator*(); } // Dereference current listener, MUST only be done after a call to IsReady(). template ILINE T CListenerNotifier::operator*() { // Ensure IsReady() was called and its return value checked CRY_ASSERT(m_pListener); // Clear cached listener pointer to force a IsReady() call before this can be called again. // This is done as the listener could be removed during any call to its own event handlers // resulting in m_pListener becoming a dangling pointer. T pListener(m_pListener); m_pListener = T(); return pListener; } // Move to next valid listener template ILINE void CListenerNotifier::Next() { size_t index = m_index; typename CListenerSet::ListenerRecord * pNextRecord = NULL; m_pListener = NULL; // Always assume there's no next, let the code below prove otherwise! const size_t listenerCount = m_listenerSet.m_listeners.size(); while (++index < listenerCount) { typename CListenerSet::ListenerRecord & record(m_listenerSet.m_listeners[index]); // Is this element valid? if (record.m_pListener) { pNextRecord = &record; break; } // Else move to next element } if (pNextRecord) { m_pListener = pNextRecord->m_pListener; #ifdef CRY_LISTENERSET_DEBUG m_szName = pNextRecord->m_szName; #endif } m_index = index; } // Returns the name of the listener (if available) template inline const char* CListenerNotifier::Name() const { #ifdef CRY_LISTENERSET_DEBUG return m_szName; #else return NULL; #endif } #endif // CRYINCLUDE_CRYCOMMON_CRYLISTENERSET_H