From 9519f73038e11d06b9611fa6d86f6f437cb3b7f8 Mon Sep 17 00:00:00 2001 From: Matthias Killat Date: Fri, 16 Sep 2022 15:44:46 +0200 Subject: [PATCH] iox-#1640 Add singleton documentation Signed-off-by: Matthias Killat --- .../include/iceoryx_dust/singleton.hpp | 253 +++++++++++------- .../test/moduletests/test_singleton.cpp | 33 +-- 2 files changed, 176 insertions(+), 110 deletions(-) diff --git a/iceoryx_dust/include/iceoryx_dust/singleton.hpp b/iceoryx_dust/include/iceoryx_dust/singleton.hpp index d2ff56cbba..11dfd99660 100644 --- a/iceoryx_dust/include/iceoryx_dust/singleton.hpp +++ b/iceoryx_dust/include/iceoryx_dust/singleton.hpp @@ -1,6 +1,23 @@ -#pragma once +// Copyright (c) 2022 by Apex.AI Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +#ifndef IOX_DUST_SINGLETON_HPP +#define IOX_DUST_SINGLETON_HPP -// TODO: decide location +// TODO: decide file location #include #include @@ -10,120 +27,166 @@ namespace iox { +/// @brief Generic class that allows accessing another class like a singleton. +/// The singleton instance is either initialized explictly or lazily default initalized on first access. +/// It is possible to destroy the singleton instance explictly (call the dtor), but it must be ensured +/// that it is not accessed anymore. Reinitialization is only possible after destruction and should be used +/// carefully (ensure instance is not used during reinitialization). +/// Initialization and destruction is thread-safe. +/// @tparam T type of the singleton instance +/// +/// @note While Singleton allows using T as a singleton instance, it cannot prevent other T instances to be +/// constructed explictly. template class Singleton { public: - static bool isInitialized() - { - return ptr().load(std::memory_order_relaxed) != nullptr; - } + Singleton(const Singleton&) = delete; + Singleton(Singleton&&) = delete; + Singleton& operator=(const Singleton&) = delete; + Singleton& operator=(Singleton&&) = delete; + + ~Singleton(); + + /// @brief check whether the singleton instance is initialized + /// @return true if the singleton instance is initialized, false otherwise + /// @note thread-safe + static bool isInitialized(); + + /// @brief construct the singleton instance if not already initialized + /// @param args consctructor arguments of the singleton instance + /// @return reference to the constructed or previously existing singleton instance + /// @note thread-safe + template + static T& init(Args&&... args); + + /// @brief explicitly destroy the singleton instance if it is initialized + /// @note thread-safe + /// @note must ONLY be called if the instance is no longer accessed + /// (this is not a severe restriction, as it is true for standard static singletons as well) + /// @note benefit: better control of the life-time of a singleton + /// @note if destroy is not explictly called, it will be called implictly after main, + /// the usual rules (and problems) of static destruction order apply + static void destroy(); + + /// @brief explicitly destroy the singleton instance if it is initialized + /// @return reference to the default constructed or previously existing singleton instance + /// @note thread-safe wrt. all functions except for destroy + /// @note accessing the returned instance is undefined behavior if called concurrently with destroy + static T& instance(); + + private: + using storage_t = typename std::aligned_storage_t::type; + + Singleton() = default; + + // avoid the external definitions of regular statics (especially with templates) + // and use lazy initialization + static auto& storage(); + + static auto& ptr(); + + static auto& lock(); template - static void init(Args&&... args) + static T* initialize(Args&&... args); +}; + +template +bool Singleton::isInitialized() +{ + return ptr().load(std::memory_order_relaxed) != nullptr; +} + +template +template +T& Singleton::init(Args&&... args) +{ + std::lock_guard g(lock()); + if (!isInitialized()) { - std::lock_guard g(lock()); - if (!isInitialized()) - { - initialize(std::forward(args)...); - } + // initialized by this call + return *initialize(std::forward(args)...); } + // initialized before by some other call + return *ptr().load(std::memory_order_acquire); +} - // must ONLY be called if it is clear no one uses the singleton anymore - // - could guard against it with additional state, - // but this slows down get() which should be fast (single load) - // - not an unusual requirement, applies to any concurrently used objects - static void destroy() +template +void Singleton::destroy() +{ + std::lock_guard g(lock()); + auto p = ptr().load(std::memory_order_acquire); + if (p) { - std::lock_guard g(lock()); - auto p = ptr().load(std::memory_order_acquire); - if (p) - { - p->~T(); - ptr().store(nullptr); - } + p->~T(); + ptr().store(nullptr); } +} - // undefined behavior if called after destroy - // - could check but this requires an additional atomic flag/enum and is not a - // good design - // - similar probelms exist with singletons that only auto destruct (design - // must ensure they are not accessed after destruction) - // - destroy allows better control of destruction - static T& get() +template +T& Singleton::instance() +{ + // need to sync the memory at *p as well + auto p = ptr().load(std::memory_order_acquire); + if (!p) { - // need to sync the memory at *p as well - auto p = ptr().load(std::memory_order_acquire); - if (!p) + std::lock_guard g(lock()); + // could have been initialized in the meantime, + // so we double check under lock + auto p = ptr().load(); + if (p) { - std::lock_guard g(lock()); - // could have been initialized in the meantime, - // so we double check under lock - auto p = ptr().load(); - if (p) - { - return *p; - } - - p = initialize(); // lazy default initialization - ptr().store(p, std::memory_order_release); - - // was initialized and stays initialized until destroy return *p; } - return *p; - } - // destroy only calls the dtor of T if it was not destroyed before - ~Singleton() - { - destroy(); - } + p = initialize(); // lazy default initialization + ptr().store(p, std::memory_order_release); - private: - Singleton() = default; - Singleton(const Singleton&) = delete; - Singleton(Singleton&&) = delete; - - using storage_t = typename std::aligned_storage_t::type; - - // avoid the external definitions of regular statics and use lazy init - static auto& storage() - { - static storage_t s; - return s; + // was initialized and stays initialized until destroy + return *p; } + return *p; +} - static auto& ptr() - { - static std::atomic p; - return p; - } +template +Singleton::~Singleton() +{ + destroy(); +} - static auto& lock() - { - static std::mutex m; - return m; - } +template +auto& Singleton::storage() +{ + static storage_t s; + return s; +} - template - static T* initialize(Args&&... args) - { - static Singleton singleton; // dtor will be called later and call destroy - auto p = new (&storage()) T(std::forward(args)...); - ptr().store(p, std::memory_order_relaxed); // memory synced by lock - return p; - } -}; +template +auto& Singleton::ptr() +{ + static std::atomic p; + return p; +} -// concurrent get: exactly one wins,locks, initializes and syncs pointer -// -// concurrent get and init: one wins and syncs pointer -// -// concurrent init and destroy: no problem but undefined outcome (object alive -// or not) -// -// concurrent get and destroy: not allowed, same with regular singleton dtor and -// access in e.g. another static +template +auto& Singleton::lock() +{ + static std::mutex m; + return m; +} -} // namespace iox \ No newline at end of file +template +template +T* Singleton::initialize(Args&&... args) +{ + static Singleton singleton; // dtor will be called later and call destroy + // NOLINTJUSTIFICATION implicit conversion from raw pointer is intentional in design of relocatable structures + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) T dtor will be called by singleton dtor + auto p = new (&storage()) T(std::forward(args)...); + ptr().store(p, std::memory_order_relaxed); // memory synced by lock + return p; +} +} // namespace iox + +#endif // IOX_DUST_SINGLETON_HPP \ No newline at end of file diff --git a/iceoryx_dust/test/moduletests/test_singleton.cpp b/iceoryx_dust/test/moduletests/test_singleton.cpp index 191c9be857..cfaed71489 100644 --- a/iceoryx_dust/test/moduletests/test_singleton.cpp +++ b/iceoryx_dust/test/moduletests/test_singleton.cpp @@ -107,8 +107,9 @@ TEST_F(Singleton_test, destroy) TEST_F(Singleton_test, defaultInit) { EXPECT_FALSE(TestSingleton::isInitialized()); - TestSingleton::init(); + auto& foo = TestSingleton::init(); + EXPECT_EQ(foo.value, DEFAULT_VALUE); EXPECT_TRUE(TestSingleton::isInitialized()); EXPECT_EQ(Foo::numDefaultCtorCalls, 1); } @@ -117,10 +118,11 @@ TEST_F(Singleton_test, initWithArguments) { constexpr uint32_t VAL{73}; EXPECT_FALSE(TestSingleton::isInitialized()); - TestSingleton::init(VAL); + auto& foo = TestSingleton::init(VAL); + EXPECT_EQ(foo.value, VAL); EXPECT_TRUE(TestSingleton::isInitialized()); - EXPECT_EQ(TestSingleton::get().value, VAL); + EXPECT_EQ(TestSingleton::instance().value, VAL); EXPECT_EQ(Foo::numCtorCalls, 1); } @@ -143,7 +145,7 @@ TEST_F(Singleton_test, reinitAfterDestroy) TestSingleton::destroy(); TestSingleton::init(VAL); - EXPECT_EQ(TestSingleton::get().value, VAL); + EXPECT_EQ(TestSingleton::instance().value, VAL); EXPECT_EQ(Foo::numDefaultCtorCalls, 1); EXPECT_EQ(Foo::numCtorCalls, 1); EXPECT_EQ(Foo::numDtorCalls, 1); @@ -157,21 +159,21 @@ TEST_F(Singleton_test, nonInitDestroyDoesNotCallDtor) EXPECT_EQ(Foo::numDtorCalls, 0); } -TEST_F(Singleton_test, nonInitGetCallsDefaultCtor) +TEST_F(Singleton_test, nonInitInstanceCallsDefaultCtor) { - auto& foo = TestSingleton::get(); + auto& foo = TestSingleton::instance(); EXPECT_TRUE(TestSingleton::isInitialized()); EXPECT_EQ(Foo::numDefaultCtorCalls, 1); EXPECT_EQ(foo.value, DEFAULT_VALUE); } -TEST_F(Singleton_test, initGetCallsNoCtor) +TEST_F(Singleton_test, initInstanceCallsNoCtor) { constexpr uint32_t VAL{73}; TestSingleton::init(VAL); EXPECT_EQ(Foo::numCtorCalls, 1); - auto& foo = TestSingleton::get(); + auto& foo = TestSingleton::instance(); EXPECT_TRUE(TestSingleton::isInitialized()); EXPECT_EQ(Foo::numCtorCalls, 1); @@ -179,10 +181,10 @@ TEST_F(Singleton_test, initGetCallsNoCtor) EXPECT_EQ(foo.value, VAL); } -TEST_F(Singleton_test, initAfterGetCallsNoCtor) +TEST_F(Singleton_test, initAfterInstanceCallsNoCtor) { constexpr uint32_t VAL{73}; - auto& foo = TestSingleton::get(); + auto& foo = TestSingleton::instance(); EXPECT_TRUE(TestSingleton::isInitialized()); EXPECT_EQ(Foo::numDefaultCtorCalls, 1); EXPECT_EQ(Foo::numCtorCalls, 0); @@ -193,10 +195,10 @@ TEST_F(Singleton_test, initAfterGetCallsNoCtor) EXPECT_EQ(foo.value, DEFAULT_VALUE); } -TEST_F(Singleton_test, multiGetCallsDefaultCtorOnce) +TEST_F(Singleton_test, multiInstanceCallsDefaultCtorOnce) { - TestSingleton::get(); - auto& foo = TestSingleton::get(); + TestSingleton::instance(); + auto& foo = TestSingleton::instance(); EXPECT_EQ(foo.value, DEFAULT_VALUE); EXPECT_EQ(Foo::numDefaultCtorCalls, 1); @@ -204,7 +206,8 @@ TEST_F(Singleton_test, multiGetCallsDefaultCtorOnce) EXPECT_EQ(Foo::numDtorCalls, 0); } -// Note: automatic destruction after main cannot be checked (cannot run tests after main) -// verify by review that destroy is called which calls the dtor unless not initialized +// Note: automatic destruction of wrapped Foo in Singleton after main cannot be checked +// (cannot run tests after main) +// verify by review that destroy is called in Singleton dtor which calls the dtor unless not initialized } // namespace \ No newline at end of file