Skip to content

Commit

Permalink
iox-eclipse-iceoryx#1640 Add singleton documentation
Browse files Browse the repository at this point in the history
Signed-off-by: Matthias Killat <[email protected]>
  • Loading branch information
MatthiasKillat committed Sep 16, 2022
1 parent 701a35d commit 9519f73
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 110 deletions.
253 changes: 158 additions & 95 deletions iceoryx_dust/include/iceoryx_dust/singleton.hpp
Original file line number Diff line number Diff line change
@@ -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 <atomic>
#include <iostream>
Expand All @@ -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<T> allows using T as a singleton instance, it cannot prevent other T instances to be
/// constructed explictly.
template <typename T>
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 <typename... Args>
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<sizeof(T), alignof(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 <typename... Args>
static void init(Args&&... args)
static T* initialize(Args&&... args);
};

template <typename T>
bool Singleton<T>::isInitialized()
{
return ptr().load(std::memory_order_relaxed) != nullptr;
}

template <typename T>
template <typename... Args>
T& Singleton<T>::init(Args&&... args)
{
std::lock_guard<std::mutex> g(lock());
if (!isInitialized())
{
std::lock_guard<std::mutex> g(lock());
if (!isInitialized())
{
initialize(std::forward<Args>(args)...);
}
// initialized by this call
return *initialize(std::forward<Args>(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 <typename T>
void Singleton<T>::destroy()
{
std::lock_guard<std::mutex> g(lock());
auto p = ptr().load(std::memory_order_acquire);
if (p)
{
std::lock_guard<std::mutex> 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 <typename T>
T& Singleton<T>::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<std::mutex> g(lock());
// could have been initialized in the meantime,
// so we double check under lock
auto p = ptr().load();
if (p)
{
std::lock_guard<std::mutex> 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<sizeof(T), alignof(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<T*> p;
return p;
}
template <typename T>
Singleton<T>::~Singleton()
{
destroy();
}

static auto& lock()
{
static std::mutex m;
return m;
}
template <typename T>
auto& Singleton<T>::storage()
{
static storage_t s;
return s;
}

template <typename... Args>
static T* initialize(Args&&... args)
{
static Singleton singleton; // dtor will be called later and call destroy
auto p = new (&storage()) T(std::forward<Args>(args)...);
ptr().store(p, std::memory_order_relaxed); // memory synced by lock
return p;
}
};
template <typename T>
auto& Singleton<T>::ptr()
{
static std::atomic<T*> 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 <typename T>
auto& Singleton<T>::lock()
{
static std::mutex m;
return m;
}

} // namespace iox
template <typename T>
template <typename... Args>
T* Singleton<T>::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>(args)...);
ptr().store(p, std::memory_order_relaxed); // memory synced by lock
return p;
}
} // namespace iox

#endif // IOX_DUST_SINGLETON_HPP
33 changes: 18 additions & 15 deletions iceoryx_dust/test/moduletests/test_singleton.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}

Expand All @@ -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);
Expand All @@ -157,32 +159,32 @@ 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);
EXPECT_EQ(Foo::numDefaultCtorCalls, 0);
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);
Expand All @@ -193,18 +195,19 @@ 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);
EXPECT_EQ(Foo::numCtorCalls, 0);
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<Foo> after main cannot be checked
// (cannot run tests after main)
// verify by review that destroy is called in Singleton<Foo> dtor which calls the dtor unless not initialized

} // namespace

0 comments on commit 9519f73

Please sign in to comment.