Skip to content

Commit 416ac57

Browse files
author
Alex Radzevich
committed
Implement External lifetime support and descriptor-keyed factories; update tests and CMake configuration
1 parent 7b210f4 commit 416ac57

12 files changed

Lines changed: 467 additions & 148 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ Makefile
3838
.DS_Store
3939
Thumbs.db
4040

41-
cmake-build-debug
41+
cmake-build-debug
42+
build_test

build_test/Testing/Temporary/CTestCostData.txt

Lines changed: 0 additions & 2 deletions
This file was deleted.

build_test/Testing/Temporary/LastTest.log

Lines changed: 0 additions & 49 deletions
This file was deleted.

build_test/tests/ioc_tests

-93.5 KB
Binary file not shown.

include/ioc/lifetime.h

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22

33
namespace IOC {
44

5-
struct Singleton {};
5+
struct Singleton {};
66

7-
struct Scoped {};
7+
struct Scoped {};
88

9-
struct Transient {};
9+
struct Transient {};
1010

11-
}
11+
// External: the instance is owned outside of the container and is supplied
12+
// at runtime via `Container::Bind<TDescriptor>(ptr)`. Resolving an External
13+
// descriptor returns a non-owning pointer to that externally-managed object.
14+
// This is the bridge between the compile-time DI graph and runtime values
15+
// (configuration, OS resources, pre-existing collaborators).
16+
struct External {};
17+
18+
} // namespace IOC

include/ioc/lifetime_manager.h

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,73 @@
44
#include "service_factory.h"
55
#include "util/evaluate_type.h"
66
#include <memory>
7-
#include <optional>
87

98
namespace IOC {
109

11-
template <typename TDescriptor, typename TLifetime = typename Binding<TDescriptor>::TLifetime>
12-
struct LifetimeManager;
10+
template <typename TDescriptor, typename TLifetime = typename Binding<TDescriptor>::TLifetime>
11+
struct LifetimeManager;
1312

14-
template <typename TDescriptor>
15-
struct LifetimeManager<TDescriptor, Transient> {
16-
using TService = Binding<TDescriptor>::TService;
13+
template <typename TDescriptor>
14+
struct LifetimeManager<TDescriptor, Transient> {
15+
using TService = typename Binding<TDescriptor>::TService;
16+
using TFactory = ResolveFactory_t<TService, TDescriptor>;
1717

18-
constexpr auto GetOrCreate(auto& container) {
19-
return ServiceFactory<TService>::Create(container);
20-
}
21-
};
18+
constexpr auto GetOrCreate(auto& container) {
19+
return TFactory::Create(container);
20+
}
21+
};
2222

23-
template <typename TDescriptor>
24-
struct LifetimeManager<TDescriptor, Scoped> {
25-
using TService = Binding<TDescriptor>::TService;
26-
using TRealType = Util::ReplaceDescriptors<TService>::TResult;
23+
template <typename TDescriptor>
24+
struct LifetimeManager<TDescriptor, Scoped> {
25+
using TService = typename Binding<TDescriptor>::TService;
26+
using TRealType = typename Util::ReplaceDescriptors<TService>::TResult;
27+
using TFactory = ResolveFactory_t<TService, TDescriptor>;
2728

28-
constexpr auto GetOrCreate(auto& container) {
29-
if (!Instance_.has_value()) [[unlikely]] {
30-
Instance_ = ServiceFactory<TService>::Create(container);
31-
}
29+
// Storage is std::unique_ptr (not std::optional) so non-movable / non-copyable
30+
// services are supported: heap-construct in place via make_unique.
31+
auto GetOrCreate(auto& container) {
32+
if (!Instance_) [[unlikely]] {
33+
Instance_ = TFactory::CreateUnique(container);
34+
}
35+
return Instance_.get();
36+
}
3237

33-
return std::addressof(Instance_.value());
34-
}
38+
private:
39+
std::unique_ptr<TRealType> Instance_;
40+
};
3541

36-
private:
37-
std::optional<TRealType> Instance_;
38-
};
42+
template <typename TDescriptor>
43+
struct LifetimeManager<TDescriptor, Singleton> {
44+
using TService = typename Binding<TDescriptor>::TService;
45+
using TFactory = ResolveFactory_t<TService, TDescriptor>;
3946

40-
template <typename TDescriptor>
41-
struct LifetimeManager<TDescriptor, Singleton> {
42-
using TService = Binding<TDescriptor>::TService;
47+
auto GetOrCreate(auto& container) {
48+
// NOTE: Singleton intentionally lives in a function-local static so
49+
// its lifetime spans the whole process and is shared across
50+
// containers. Per-container lifetime should use Scoped.
51+
static auto instance = TFactory::Create(container);
52+
return std::addressof(instance);
53+
}
54+
};
4355

44-
auto GetOrCreate(auto& container) {
45-
// TODO: Definition of a static variable in a constexpr function is a C++23 extension
46-
static auto instance = ServiceFactory<TService>::Create(container);
47-
return std::addressof(instance);
48-
}
49-
};
56+
// External: the container does not own the instance. The user must call
57+
// Container::Bind<TDescriptor>(ptr) before the first Resolve. GetOrCreate
58+
// returns the registered pointer (or nullptr if Bind was never called).
59+
template <typename TDescriptor>
60+
struct LifetimeManager<TDescriptor, External> {
61+
using TService = typename Binding<TDescriptor>::TService;
62+
using TRealType = typename Util::ReplaceDescriptors<TService>::TResult;
5063

51-
}
64+
constexpr TRealType* GetOrCreate(auto&) const noexcept {
65+
return Instance_;
66+
}
67+
68+
constexpr void SetInstance(TRealType* ptr) noexcept {
69+
Instance_ = ptr;
70+
}
71+
72+
private:
73+
TRealType* Instance_ = nullptr;
74+
};
75+
76+
} // namespace IOC

include/ioc/service_collection.h

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,53 @@
33
#include "binding.h"
44
#include "lifetime_manager.h"
55

6-
namespace IOC {
7-
8-
template <typename TDescriptor, typename... TDescriptors>
9-
concept OneOf = (std::same_as<TDescriptor, TDescriptors> || ...);
10-
11-
template <typename ...TDescriptors>
12-
class Container;
13-
14-
template <typename ...TDescriptors>
15-
class Container {
16-
private:
17-
std::tuple<LifetimeManager<TDescriptors>...> LifetimeManagers_;
6+
#include <concepts>
7+
#include <optional>
8+
#include <tuple>
9+
#include <type_traits>
1810

19-
public:
20-
template <OneOf<TDescriptors...> TDescriptor>
21-
constexpr auto Resolve() {
22-
return std::get<LifetimeManager<TDescriptor>>(LifetimeManagers_)
23-
.GetOrCreate(*this);
24-
}
25-
26-
template <typename TDescriptor>
27-
constexpr auto Resolve() {
28-
return std::nullopt;
29-
}
30-
};
31-
32-
template <typename ...TContainerArs, typename ...Rest>
33-
class Container<Container<TContainerArs...>, Rest...>
34-
: public Container<Rest..., TContainerArs...> {
35-
};
11+
namespace IOC {
3612

37-
}
13+
template <typename TDescriptor, typename... TDescriptors>
14+
concept OneOf = (std::same_as<TDescriptor, TDescriptors> || ...);
15+
16+
template <typename... TDescriptors>
17+
class Container;
18+
19+
template <typename... TDescriptors>
20+
class Container {
21+
private:
22+
std::tuple<LifetimeManager<TDescriptors>...> LifetimeManagers_;
23+
24+
public:
25+
template <OneOf<TDescriptors...> TDescriptor>
26+
constexpr auto Resolve() {
27+
return std::get<LifetimeManager<TDescriptor>>(LifetimeManagers_).GetOrCreate(*this);
28+
}
29+
30+
template <typename TDescriptor>
31+
constexpr auto Resolve() {
32+
// Unregistered descriptor. Returning std::nullopt keeps SFINAE-style
33+
// probes working but resolving a missing descriptor in production code
34+
// is almost always a wiring bug.
35+
return std::nullopt;
36+
}
37+
38+
// Register an externally-owned instance for an `External`-lifetime
39+
// descriptor. Must be called before the first Resolve<TDescriptor>().
40+
// Compile error if TDescriptor is not registered or is not External.
41+
template <OneOf<TDescriptors...> TDescriptor, typename T>
42+
constexpr void Bind(T* ptr) {
43+
using Manager = LifetimeManager<TDescriptor>;
44+
static_assert(std::is_same_v<typename Binding<TDescriptor>::TLifetime, External>,
45+
"Container::Bind<D>(ptr) only applies to descriptors with "
46+
"TLifetime = IOC::External.");
47+
std::get<Manager>(LifetimeManagers_).SetInstance(ptr);
48+
}
49+
};
50+
51+
template <typename... TContainerArs, typename... Rest>
52+
class Container<Container<TContainerArs...>, Rest...> :
53+
public Container<Rest..., TContainerArs...> {};
54+
55+
} // namespace IOC

include/ioc/service_factory.h

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,99 @@
11
#pragma once
22

3-
#include <type_traits>
43
#include "util/evaluate_type.h"
4+
#include <memory>
5+
#include <type_traits>
6+
#include <utility>
57

68
namespace IOC {
79

8-
template <typename TService>
10+
// Factory keyed by both the service type AND the requesting descriptor.
11+
// The descriptor parameter defaults to `void` so the common case ("one
12+
// factory per service type") keeps the simpler descriptor-less form.
13+
// Specialise with an explicit descriptor when several descriptors share
14+
// the same service type but need different construction recipes (e.g.
15+
// multiple `SpScBytesQueue` instances with different names/capacities).
16+
//
17+
// Lookup precedence (see ResolveFactory_t below):
18+
// 1. ServiceFactory<TService, TDescriptor> (user-provided per-descriptor)
19+
// 2. ServiceFactory<TService, void> (auto / default)
20+
template <typename TService, typename TDescriptor = void>
921
struct ServiceFactory;
1022

23+
// Default specialisation: descriptor-less, default-constructs the service.
1124
template <typename TService>
12-
struct ServiceFactory {
25+
struct ServiceFactory<TService, void> {
1326
static constexpr TService Create(auto&) {
14-
static_assert(std::is_default_constructible_v<TService>);
27+
static_assert(std::is_default_constructible_v<TService>,
28+
"Service has no descriptor parameters and is not default-constructible. "
29+
"Specialise IOC::ServiceFactory<TService> (or "
30+
"IOC::ServiceFactory<TService, TDescriptor>) with a custom Create().");
1531
return TService();
1632
}
33+
34+
static auto CreateUnique(auto& container) {
35+
if constexpr (std::is_default_constructible_v<TService>) {
36+
return std::make_unique<TService>();
37+
} else {
38+
return std::unique_ptr<TService>(new TService(Create(container)));
39+
}
40+
}
1741
};
1842

19-
template <template<typename ...> class TService, typename... TDescriptors>
20-
struct ServiceFactory<TService<TDescriptors...>> {
43+
// Templated-service specialisation: resolves each TDescriptor from the
44+
// container and forwards it to the actual constructor.
45+
template <template <typename...> class TService, typename... TDescriptors>
46+
struct ServiceFactory<TService<TDescriptors...>, void> {
47+
using TActualServiceType =
48+
typename Util::ReplaceDescriptors<TService<TDescriptors...>>::TResult;
49+
2150
static constexpr auto Create(auto& container) {
22-
using TActualServiceType = typename Util::ReplaceDescriptors<TService<TDescriptors...>>::TResult;
51+
static_assert(
52+
std::is_constructible_v<TActualServiceType,
53+
decltype(container.template Resolve<TDescriptors>())...>,
54+
"Service is not constructible from its resolved dependencies. "
55+
"Either fix the constructor signature or specialise IOC::ServiceFactory<...>.");
56+
return TActualServiceType(container.template Resolve<TDescriptors>()...);
57+
}
2358

24-
if constexpr (std::is_constructible_v<TActualServiceType, decltype(container.template Resolve<TDescriptors>())...>) {
25-
return TActualServiceType(container.template Resolve<TDescriptors>()...);
26-
} else {
27-
return TActualServiceType();
28-
}
59+
static auto CreateUnique(auto& container) {
60+
static_assert(
61+
std::is_constructible_v<TActualServiceType,
62+
decltype(container.template Resolve<TDescriptors>())...>,
63+
"Service is not constructible from its resolved dependencies (CreateUnique).");
64+
return std::make_unique<TActualServiceType>(
65+
container.template Resolve<TDescriptors>()...);
2966
}
3067
};
3168

69+
// Selects the matching ServiceFactory specialisation for a (Service,
70+
// Descriptor) pair. If the user specialised
71+
// `ServiceFactory<TService, TDescriptor>` it is picked; otherwise the
72+
// descriptor-less `ServiceFactory<TService, void>` is used.
73+
//
74+
// Detection: the primary `ServiceFactory<TService, TDescriptor>` is only
75+
// forward-declared, so `sizeof(ServiceFactory<T, D>)` is valid *only* when
76+
// (a) D == void (the default partial spec is complete), or (b) the user
77+
// supplied a descriptor-keyed specialisation. Probe via SFINAE and use a
78+
// descriptor-keyed factory only when D != void.
79+
namespace detail {
80+
81+
template <typename TService, typename TDescriptor, typename = void>
82+
struct HasDescriptorKeyedFactory : std::false_type {};
83+
84+
template <typename TService, typename TDescriptor>
85+
struct HasDescriptorKeyedFactory<
86+
TService,
87+
TDescriptor,
88+
std::void_t<decltype(sizeof(ServiceFactory<TService, TDescriptor>))>>
89+
: std::bool_constant<!std::is_void_v<TDescriptor>> {};
90+
91+
} // namespace detail
92+
93+
template <typename TService, typename TDescriptor>
94+
using ResolveFactory_t =
95+
std::conditional_t<detail::HasDescriptorKeyedFactory<TService, TDescriptor>::value,
96+
ServiceFactory<TService, TDescriptor>,
97+
ServiceFactory<TService, void>>;
98+
3299
}

0 commit comments

Comments
 (0)