diff --git a/mapbox/CMakeLists.txt b/mapbox/CMakeLists.txt index 4ffc002..c8d407f 100644 --- a/mapbox/CMakeLists.txt +++ b/mapbox/CMakeLists.txt @@ -29,3 +29,4 @@ mapbox_base_add_library(optional ${CMAKE_CURRENT_LIST_DIR}/optional) mapbox_base_add_library(pixelmatch-cpp ${CMAKE_CURRENT_LIST_DIR}/pixelmatch-cpp/include) mapbox_base_add_library(supercluster.hpp ${CMAKE_CURRENT_LIST_DIR}/supercluster.hpp/include) mapbox_base_add_library(variant ${CMAKE_CURRENT_LIST_DIR}/variant/include) +mapbox_base_add_library(weak ${CMAKE_CURRENT_LIST_DIR}/weak/include) diff --git a/mapbox/weak/LICENSE b/mapbox/weak/LICENSE new file mode 100644 index 0000000..6c4ce40 --- /dev/null +++ b/mapbox/weak/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) MapBox +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. +- Neither the name "MapBox" nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/mapbox/weak/README.md b/mapbox/weak/README.md new file mode 100644 index 0000000..fd4658e --- /dev/null +++ b/mapbox/weak/README.md @@ -0,0 +1,4 @@ +# mapbox-weak +Mapbox weak pointer and weak pointer factory + +Generic API for weak pointer classes. diff --git a/mapbox/weak/include/mapbox/weak.hpp b/mapbox/weak/include/mapbox/weak.hpp new file mode 100644 index 0000000..237f511 --- /dev/null +++ b/mapbox/weak/include/mapbox/weak.hpp @@ -0,0 +1,348 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mapbox { +namespace base { + +/// @cond internal +namespace internal { + +class WeakPtrSharedData { +public: + WeakPtrSharedData() = default; + + void sharedLock() { + mutex_.lock_shared(); + } + + void sharedUnlock() { + mutex_.unlock_shared(); + } + + void invalidate() { + std::lock_guard lock(mutex_); + valid_ = false; + } + + bool valid() const { return valid_; } + +private: + std::shared_timed_mutex mutex_; // Blocks on WeakPtrFactory destruction. + std::atomic valid_{true}; +}; + +using StrongRef = std::shared_ptr; +using WeakRef = std::weak_ptr; + +template +class WeakPtrBase; + +} // namespace internal +/// @endcond + +/** + * @brief Scope guard for the WeakPtr-wrapped object + * + * The WeakPtr-wrapped object is guaranteed not to be deleted while + * a WeakPtrGuard instance is present in the scope. + */ +class WeakPtrGuard { +public: + /** + * @brief Default move constructor + */ + WeakPtrGuard(WeakPtrGuard&&) noexcept = default; + ~WeakPtrGuard() { + if (strong_) { + strong_->sharedUnlock(); + } + } + +private: + explicit WeakPtrGuard(internal::StrongRef strong) : strong_(std::move(strong)) { + assert(!strong_ || strong_->valid()); + } + internal::StrongRef strong_; + + template + friend class internal::WeakPtrBase; +}; + +namespace internal { + +/** + * @brief Base type for \c WeakPtr class. + * + * Contains the generic API for weak pointer classes. + * + * This class helps to create a \c WeakPtr template specialization for a + * certain type, enabling the type-specific semantics. + * + * \sa WeakPtr + * + * @tparam Object the managed object type + */ +template +class WeakPtrBase { +public: + /** + * Gets a lock that protects the Object, giving + * the guarantee that it will not be deleted (if not + * deleted yet). + * + * Note that it won't make the Object thread-safe, but + * rather make sure it exists (or not) when the lock + * is being held. + * + * Note: there *MUST* be only one instance of the + * guard referring to the same \a WeakPtrFactory + * available in the scope at a time. + */ + WeakPtrGuard lock() const { + if (StrongRef strong = weak_.lock()) { + strong->sharedLock(); + if (strong->valid()) { + return WeakPtrGuard(std::move(strong)); + } + strong->sharedUnlock(); + } + return WeakPtrGuard(nullptr); + } + + /** + * @brief Quick nonblocking check that the wrapped Object still exists. + * + * Checks if the weak pointer is still pointing to a valid + * object. Note that if the WeakPtrFactory lives in a different + * thread, a `false` result cannot be guaranteed to be + * correct since there is an implicit race condition, + * but a `true` result is always correct. + * + * @return given the thread restrictions, true if expired, + * false otherwise. + */ + bool expired() const { + if (StrongRef strong = weak_.lock()) { + return !strong->valid(); + } + return true; + } + + /** + * @brief Quick nonblocking check that the wrapped Object still exists. + * + * \sa expired() + * + * @return given the thread restrictions, true if the object exists, + * false otherwise. + */ + explicit operator bool() const { return !expired(); } + +protected: + /** + * Get a pointer to the wrapped object. + * The caller *MUST* call lock() and keep locker, then + * check if it was nulled before using it. + * + * Usage should be as brief as possible, because it might + * potentially block the thread where the Object lives. + * + * @return pointer to the object, nullptr if expired. + */ + Object* object() const { + if (StrongRef strong = weak_.lock()) { + if (strong->valid()) { + return ptr_; + } + } + return nullptr; + } + /// @cond internal + WeakPtrBase() = default; + WeakPtrBase(WeakPtrBase&&) noexcept = default; + WeakPtrBase(const WeakPtrBase&) noexcept = default; + template // NOLINTNEXTLINE + WeakPtrBase(WeakPtrBase&& other) noexcept + : weak_(std::move(other.weak_)), ptr_(static_cast(other.ptr_)) {} + explicit WeakPtrBase(WeakRef weak, Object* ptr) : weak_(std::move(weak)), ptr_(ptr) { assert(ptr_); } + WeakPtrBase& operator=(WeakPtrBase&& other) noexcept = default; + WeakPtrBase& operator=(const WeakPtrBase& other) = default; + + ~WeakPtrBase() = default; + +private: + WeakRef weak_; + Object* ptr_{}; + template + friend class WeakPtrBase; + /// @endcond +}; + +} // namespace internal + +/** + * @brief Default implementation of a weak pointer to an object. + * + * Weak pointers are safe to access even if the + * pointer outlives the Object this class wraps. + * + * This class will manage only object lifetime + * but will not deal with thread-safeness of the + * objects it is wrapping. + */ +template +class WeakPtr final : public internal::WeakPtrBase { +public: + /** + * @brief Default constructor. + * + * Constructs empty \c WeakPtr. + */ + WeakPtr() = default; + + /** + * @brief Converting move constructor + * + * \a other becomes empty after the call. + * + * @tparam U a type, which \c Object is convertible to + * @param other \c WeakPtr instance + */ + template // NOLINTNEXTLINE + WeakPtr(WeakPtr&& other) noexcept : internal::WeakPtrBase(std::move(other)) {} + + /** + * @brief Default move constructor. + */ + WeakPtr(WeakPtr&&) noexcept = default; + + /** + * @brief Default copy constructor. + */ + WeakPtr(const WeakPtr&) noexcept = default; + + /** + * @brief Replaces the managed object with the one managed by \a other. + * + * \a other becomes empty after the call. + * + * @param other + * @return WeakPtr& \c *this + */ + WeakPtr& operator=(WeakPtr&& other) noexcept = default; + + /** + * @brief Replaces the managed object with the one managed by \a other. + * + * @param other + * @return WeakPtr& \c *this + */ + WeakPtr& operator=(const WeakPtr& other) = default; + + /** + * @brief Dereferences the stored pointer. + * + * Must not be called on empty \c WeakPtr. + * + * @return Object* the stored pointer. + */ + Object* operator->() const { + Object* ptr = this->object(); + assert(ptr); + return ptr; + } + +private: + explicit WeakPtr(internal::WeakRef weak, Object* object) : internal::WeakPtrBase(std::move(weak), object) {} + + template + friend class WeakPtrFactory; +}; + +/** + * @brief Object wrapper that can create weak pointers. + * + * WARNING: the WeakPtrFactory should all be at the bottom of + * the list of member of the class, making it the first to + * be destroyed and the last to be initialized. + */ +template +class WeakPtrFactory final { +public: + WeakPtrFactory(const WeakPtrFactory&) = delete; + WeakPtrFactory& operator=(const WeakPtrFactory&) = delete; + + /** + * @brief Construct a new \c WeakPtrFactory object. + * + * @param obj an \c Object instance to wrap. + */ + explicit WeakPtrFactory(Object* obj) : strong_(std::make_shared()), obj_(obj) {} + + /** + * Destroys the factory, invalidating all the + * weak pointers to this object, i.e. makes them empty. + */ + ~WeakPtrFactory() { strong_->invalidate(); } + + /** + * Make a weak pointer for this WeakPtrFactory. Weak pointer + * can be used for safely accessing the Object and not worry + * about lifetime. + * + * @return a weak pointer. + */ + WeakPtr makeWeakPtr() { return WeakPtr{strong_, obj_}; } + + /** + * @brief Makes a weak wrapper for calling a method on the wrapped + * \c Object instance. + * + * While the wrapped \c Object instance exists, calling the returned wrapper is + * equivalent to invoking \a method on the instance. Note that the instance deletion + * is blocked during the wrapper call. + * + * If the wrapped \c Object instance does not exist, calling the returned wrapper + * is ignored. + * + * The example below illustrates creating an \c std::function instance from the + * returned wrapper. + * + * \code + * class Object { + * void foo(int); + * std::function makeWeakFoo() { + * return weakFactory.makeWeakMethod(&Object::foo); + * } + * mbauto::WeakPtrFactory weakFactory{this}; + * }; + * \endcode + * + * @param method Pointer to an \c Object class method. + * @return auto Callable object + */ + template + auto makeWeakMethod(Method method) { + return [weakPtr = makeWeakPtr(), method](auto&&... params) mutable { + WeakPtrGuard guard = weakPtr.lock(); + if (Object* obj = weakPtr.object()) { + (obj->*method)(std::forward(params)...); + } + }; + } + +private: + internal::StrongRef strong_; + Object* obj_; +}; + +} // namespace base +} // namespace mapbox diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 81d009e..2c92692 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -31,6 +31,7 @@ target_link_libraries(include-test-targets PRIVATE Mapbox::Base::pixelmatch-cpp Mapbox::Base::supercluster.hpp Mapbox::Base::variant + Mapbox::Base::weak ) target_include_directories(include-test-targets SYSTEM PRIVATE @@ -41,6 +42,7 @@ add_test(NAME include-test-bundle COMMAND include-test-bundle) add_test(NAME include-test-targets COMMAND include-test-targets) add_executable(io-test ${CMAKE_CURRENT_LIST_DIR}/io.cpp) +add_executable(weak-test ${CMAKE_CURRENT_LIST_DIR}/weak.cpp) target_link_libraries(io-test PRIVATE Mapbox::Base::io @@ -48,7 +50,13 @@ target_link_libraries(io-test PRIVATE Mapbox::Base::Extras::filesystem ) +target_link_libraries(weak-test PRIVATE + Mapbox::Base::weak + pthread +) + add_test(NAME io-test COMMAND io-test) +add_test(NAME weak-test COMMAND weak-test) add_definitions(-DTEST_FIXTURES_PATH="${CMAKE_CURRENT_LIST_DIR}/fixtures/") add_definitions(-DTEST_BINARY_PATH="${CMAKE_CURRENT_BINARY_DIR}/") diff --git a/test/weak.cpp b/test/weak.cpp new file mode 100644 index 0000000..06bafb9 --- /dev/null +++ b/test/weak.cpp @@ -0,0 +1,121 @@ +#include + +#include +#include +#include +#include + +namespace { + +void testLock() { + using namespace std::chrono_literals; + static std::atomic_int g_i; + struct TestLock { + void inc() { ++g_i; } + mapbox::base::WeakPtrFactory factory{this}; + }; + + auto t = std::make_unique(); + auto weak1 = t->factory.makeWeakPtr(); + auto weak2 = t->factory.makeWeakPtr(); + auto weak3 = weak2; + + std::thread thread1([&] { + auto guard = weak1.lock(); + std::this_thread::sleep_for(150ms); + weak1->inc(); + }); + std::thread thread2([&] { + auto guard = weak2.lock(); + std::this_thread::sleep_for(200ms); + weak2->inc(); + }); + { + auto guard = weak3.lock(); + std::this_thread::sleep_for(50ms); + weak3->inc(); + } + + assert(!weak1.expired()); + assert(!weak2.expired()); + assert(weak1); + assert(weak2); + t.reset(); // Should not crash. + thread1.join(); + thread2.join(); + + assert(weak1.expired()); + assert(weak2.expired()); + assert(!weak1); + assert(!weak2); + assert(g_i == 3); +} + +void testWeakMethod() { + using namespace std::chrono_literals; + static std::atomic_int g_i; + class Test { + public: + void increaseGlobal(int delta) { g_i += delta; } + std::function makeWeakIncreaseGlobal() { return factory.makeWeakMethod(&Test::increaseGlobal); } + + private: + mapbox::base::WeakPtrFactory factory{this}; + }; + + auto t = std::make_unique(); + std::function weak1 = t->makeWeakIncreaseGlobal(); + std::function weak2 = t->makeWeakIncreaseGlobal(); + std::function weak3 = weak2; + + std::thread thread1([&] { weak1(1); }); + std::thread thread2([&] { weak2(10); }); + std::this_thread::sleep_for(50ms); + weak3(100); + + t.reset(); // Should not crash. + // The following calls are ignored. + weak1(1); + weak2(2); + weak3(3); + thread1.join(); + thread2.join(); + + assert(g_i == 111); +} + +void testWeakMethodBlock() { + using namespace std::chrono; + using namespace std::chrono_literals; + static std::atomic_bool g_call_finished{false}; + struct Test { + void block(decltype(1ms) duration) { + std::this_thread::sleep_for(duration); + g_call_finished = true; + } + mapbox::base::WeakPtrFactory factory{this}; + }; + + auto t = std::make_unique(); + auto weak = t->factory.makeWeakMethod(&Test::block); + auto first = high_resolution_clock::now(); + + std::thread thread([&] { weak(100ms); }); + std::this_thread::sleep_for(10ms); + t.reset(); // Deletion is blocked until weak(100ms) call returns. + thread.join(); + auto totalTime = duration_cast(high_resolution_clock::now() - first); + + assert(g_call_finished); + assert(totalTime >= 100ms); +} + +} // namespace + +int main() { + testLock(); + testWeakMethod(); + testWeakMethodBlock(); + return 0; +} +