Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions Src/Concerto/Core/Signal/Connection.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// Created by arthur on 27/02/2026.
//

#ifndef CONCERTO_CORE_CONNECTION_HPP
#define CONCERTO_CORE_CONNECTION_HPP

#include <functional>
#include <memory>

namespace cct
{
/// Non-owning handle to a signal connection.
/// Call Disconnect() to manually remove the slot from the signal.
/// Safe to use after the Signal is destroyed (becomes a no-op).
class Connection
{
public:
Connection() = default;
Connection(const Connection&) = default;
Connection(Connection&&) = default;
Connection& operator=(const Connection&) = default;
Connection& operator=(Connection&&) = default;
~Connection() = default;

inline void Disconnect();
[[nodiscard]] inline bool IsConnected() const;

private:
explicit Connection(std::function<void()> disconnectFn, std::weak_ptr<void> weakSignal) :
m_disconnectFn(std::move(disconnectFn)),
m_weakSignal(std::move(weakSignal))
{
}

template<typename... Args>
friend class Signal;

std::function<void()> m_disconnectFn;
std::weak_ptr<void> m_weakSignal;
};

/// RAII wrapper around Connection. Automatically disconnects when destroyed.
/// Movable, non-copyable.
class ScopedConnection
{
public:
ScopedConnection() = default;
explicit ScopedConnection(Connection connection) :
m_connection(std::move(connection))
{
}
~ScopedConnection() { m_connection.Disconnect(); }

ScopedConnection(const ScopedConnection&) = delete;
ScopedConnection& operator=(const ScopedConnection&) = delete;

ScopedConnection(ScopedConnection&&) = default;
ScopedConnection& operator=(ScopedConnection&& other) noexcept
{
if (this != &other)
{
m_connection.Disconnect();
m_connection = std::move(other.m_connection);
}
return *this;
}

inline void Disconnect();
[[nodiscard]] inline bool IsConnected() const;
[[nodiscard]] inline Connection Release();

private:
Connection m_connection;
};
} // namespace cct

#include "Concerto/Core/Signal/Connection.inl"

#endif // CONCERTO_CORE_CONNECTION_HPP
42 changes: 42 additions & 0 deletions Src/Concerto/Core/Signal/Connection.inl
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// Created by arthur on 27/02/2026.
//

#ifndef CONCERTO_CORE_CONNECTION_INL
#define CONCERTO_CORE_CONNECTION_INL

namespace cct
{
inline void Connection::Disconnect()
{
if (m_disconnectFn)
{
m_disconnectFn();
m_disconnectFn = nullptr;
}
}

inline bool Connection::IsConnected() const
{
return m_disconnectFn && !m_weakSignal.expired();
}

inline void ScopedConnection::Disconnect()
{
m_connection.Disconnect();
}

inline bool ScopedConnection::IsConnected() const
{
return m_connection.IsConnected();
}

inline Connection ScopedConnection::Release()
{
Connection c = std::move(m_connection);
m_connection = Connection{};
return c;
}
} // namespace cct

#endif // CONCERTO_CORE_CONNECTION_INL
89 changes: 89 additions & 0 deletions Src/Concerto/Core/Signal/Signal.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// Created by arthur on 27/02/2026.
//

#ifndef CONCERTO_CORE_SIGNAL_HPP
#define CONCERTO_CORE_SIGNAL_HPP

#include <functional>
#include <memory>
#include <vector>

#include "Concerto/Core/Signal/Connection.hpp"

namespace cct
{
/// Type-safe signal that can be connected to zero or more slots (callbacks).
///
/// - Lazy allocation: no heap allocation until the first Connect() call.
/// - Move-safe: connections remain valid after a Signal is moved.
/// - Copy-safe: copying a Signal produces a Signal with NO connections (fresh).
/// - Reentrant-safe: disconnecting inside Emit() is deferred until after emission.
/// - Lifetime-safe: Connection::Disconnect() is a no-op if the Signal was destroyed.
///
/// Usage:
/// Signal<int> onChanged;
/// auto conn = onChanged.Connect([](int v) { ... });
/// auto conn2 = onChanged.Connect(myObj, &MyClass::OnChanged);
/// onChanged.Emit(42);
/// conn.Disconnect();
template<typename... Args>
class Signal
{
public:
Signal() = default;

Signal(const Signal&) :
m_state(nullptr)
{
}
Signal& operator=(const Signal&)
{
return *this; // intentionally do not copy connections
}

Signal(Signal&&) = default;
Signal& operator=(Signal&&) = default;

~Signal() = default;

[[nodiscard]] Connection Connect(std::function<void(Args...)> slot);
template<typename T>
[[nodiscard]] Connection Connect(T* obj, void (T::*method)(Args...));
template<typename T>
[[nodiscard]] Connection Connect(const T* obj, void (T::*method)(Args...) const);

void Disconnect(Connection& connection);
void DisconnectAll();

void Emit(Args... args) const;
void operator()(Args... args) const;

[[nodiscard]] std::size_t GetConnectionCount() const;
[[nodiscard]] bool HasConnections() const;

private:
struct Slot
{
std::size_t id;
std::function<void(Args...)> fn;
bool active = true;
};

struct SlotList
{
std::vector<Slot> slots;
std::size_t nextId = 0;
int emitDepth = 0;
};

void EnsureState() const;
void DisconnectById(std::size_t id) const;

mutable std::shared_ptr<SlotList> m_state; // null until first Connect()
};
} // namespace cct

#include "Concerto/Core/Signal/Signal.inl"

#endif // CONCERTO_CORE_SIGNAL_HPP
151 changes: 151 additions & 0 deletions Src/Concerto/Core/Signal/Signal.inl
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//
// Created by arthur on 27/02/2026.
//

#ifndef CONCERTO_CORE_SIGNAL_INL
#define CONCERTO_CORE_SIGNAL_INL

#include <algorithm>

namespace cct
{
template<typename... Args>
void Signal<Args...>::EnsureState() const
{
if (!m_state)
m_state = std::make_shared<SlotList>();
}

template<typename... Args>
void Signal<Args...>::DisconnectById(std::size_t id) const
{
if (!m_state)
return;
SlotList& state = *m_state;
for (Slot& slot : state.slots)
{
if (slot.id != id)
continue;
if (state.emitDepth > 0)
{
// Deferred removal: mark inactive and clean up after Emit()
slot.active = false;
}
else
{
state.slots.erase(
std::remove_if(state.slots.begin(), state.slots.end(),
[id](const Slot& s) { return s.id == id; }),
state.slots.end());
}
return;
}
}

template<typename... Args>
Connection Signal<Args...>::Connect(std::function<void(Args...)> slot)
{
EnsureState();
const std::size_t id = m_state->nextId++;
m_state->slots.push_back({id, std::move(slot), true});

std::weak_ptr<SlotList> weakState = m_state;
std::weak_ptr<void> weakSignal = m_state;
return Connection(
[weakState, id]()
{
if (auto state = weakState.lock())
{
for (auto& s : state->slots)
{
if (s.id != id)
continue;
if (state->emitDepth > 0)
s.active = false;
else
state->slots.erase(
std::remove_if(state->slots.begin(), state->slots.end(),
[id](const Slot& e) { return e.id == id; }),
state->slots.end());
return;
}
}
},
std::move(weakSignal));
}

template<typename... Args>
template<typename T>
Connection Signal<Args...>::Connect(T* obj, void (T::*method)(Args...))
{
return Connect([obj, method](Args... args) { (obj->*method)(args...); });
}

template<typename... Args>
template<typename T>
Connection Signal<Args...>::Connect(const T* obj, void (T::*method)(Args...) const)
{
return Connect([obj, method](Args... args) { (obj->*method)(args...); });
}

template<typename... Args>
void Signal<Args...>::Disconnect(Connection& connection)
{
connection.Disconnect();
}

template<typename... Args>
void Signal<Args...>::DisconnectAll()
{
if (m_state)
m_state->slots.clear();
}

template<typename... Args>
void Signal<Args...>::Emit(Args... args) const
{
if (!m_state)
return;

SlotList& state = *m_state;
++state.emitDepth;
for (const Slot& slot : state.slots)
{
if (slot.active)
slot.fn(args...);
}
--state.emitDepth;

if (state.emitDepth == 0)
{
state.slots.erase(
std::remove_if(state.slots.begin(), state.slots.end(),
[](const Slot& s) { return !s.active; }),
state.slots.end());
}
}

template<typename... Args>
void Signal<Args...>::operator()(Args... args) const
{
Emit(args...);
}

template<typename... Args>
std::size_t Signal<Args...>::GetConnectionCount() const
{
if (!m_state)
return 0;
return static_cast<std::size_t>(
std::count_if(m_state->slots.begin(), m_state->slots.end(),
[](const Slot& s) { return s.active; }));
}

template<typename... Args>
bool Signal<Args...>::HasConnections() const
{
return GetConnectionCount() > 0;
}
} // namespace cct

#endif // CONCERTO_CORE_SIGNAL_INL
Loading
Loading