diff --git a/README.md b/README.md index 75a36c3..0b8b12a 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ See the [inversify-cpp-visualizer](https://github.com/mosure/inversify-cpp-visua ## Features * Constant, dynamic, and automatic resolvers -* Singleton, resolution (TODO), and unique scopes +* Singleton, resolution, and unique scopes ## Documentation @@ -142,12 +142,25 @@ container.bind().toConstantValue(1.618); Dynamic bindings are resolved when calling `container.get...`. -By default, dynamic bindings have resolution scope (e.g. each call to `container.get...` calls the factory). +By default, dynamic bindings have unique scope (e.g. each call to `container.get...` calls the factory). -Singleton scope dynamic bindings cache the first resolution of the binding. +Resolution scope dynamic bindings cache values during a single resolution graph, ensuring duplicate dependencies resolve to the same value. Use `.inResolutionScope()` to enable this behaviour. + +Singleton scope dynamic bindings cache the first resolution of the binding across all calls. ```cpp +container.bind().toDynamicValue( + [](auto& ctx) { + auto foo = ctx.container.template get(); + auto bar = ctx.container.template get(); + + auto fizz = std::make_shared(foo, bar); + + return fizz; + } +).inResolutionScope(); + container.bind().toDynamicValue( [](auto& ctx) { auto foo = ctx.container.template get(); diff --git a/example/simple/src/main.cpp b/example/simple/src/main.cpp index d5747f4..049c8ef 100644 --- a/example/simple/src/main.cpp +++ b/example/simple/src/main.cpp @@ -1,4 +1,8 @@ -#include +#include +#include +#include + +#include #include @@ -8,22 +12,53 @@ #include -namespace inversify = mosure::inversify; - -int main() { - inversify::Container< - symbols::logger, - symbols::service, - symbols::settings - > container; - - container.bind().to().inSingletonScope(); - container.bind().to(); - container.bind().to().inSingletonScope(); - - //container.bind(symbols::logger).to().inSingletonScope(); - - container.get()->run(); - - return 0; -} +namespace inversify = mosure::inversify; + +struct RequestLog { + RequestLog(ILoggerPtr primary, ILoggerPtr secondary) + : primary_(std::move(primary)), secondary_(std::move(secondary)) + { } + + ILoggerPtr primary_; + ILoggerPtr secondary_; +}; + +using RequestLogPtr = std::shared_ptr; + +namespace request_symbols { + using batchedLog = inversify::Symbol; +} + +template <> +struct inversify::Injectable + : inversify::Inject< + symbols::logger, + symbols::logger + > +{ }; + +int main() { + inversify::Container< + symbols::logger, + symbols::service, + symbols::settings, + request_symbols::batchedLog + > container; + + container.bind().to().inResolutionScope(); + container.bind().to(); + container.bind().to().inSingletonScope(); + container.bind().to(); + + //container.bind(symbols::logger).to().inSingletonScope(); + + container.get()->run(); + + auto requestLog = container.get(); + std::cout << std::boolalpha + << "Resolution scoped logger reused within request: " + << (requestLog->primary_ == requestLog->secondary_) + << std::endl; + + return 0; +} diff --git a/include/mosure/binding.hpp b/include/mosure/binding.hpp index da49b05..5913d74 100644 --- a/include/mosure/binding.hpp +++ b/include/mosure/binding.hpp @@ -18,6 +18,10 @@ class BindingScope { resolver_ = std::make_shared>(resolver_); } + void inResolutionScope() { + resolver_ = std::make_shared>(resolver_); + } + #ifdef INVERSIFY_BINDING_INSPECTION auto getResolver() const { return resolver_; diff --git a/include/mosure/container.hpp b/include/mosure/container.hpp index bbd33de..d80a466 100644 --- a/include/mosure/container.hpp +++ b/include/mosure/container.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -51,9 +52,21 @@ class Container "inversify::Container symbol not registered" ); - return std::get< + auto* previousScope = context_.resolutionScope; + std::optional scope; + + if (previousScope == nullptr) { + scope.emplace(); + context_.resolutionScope = &scope.value(); + } + + auto result = std::get< inversify::Binding >(bindings_).resolve(context_); + + context_.resolutionScope = previousScope; + + return result; } private: diff --git a/include/mosure/context.hpp b/include/mosure/context.hpp index 1365fa1..a1d7fc0 100644 --- a/include/mosure/context.hpp +++ b/include/mosure/context.hpp @@ -1,16 +1,60 @@ #pragma once +#include +#include +#include + #include namespace mosure::inversify { +class ResolutionScope { + struct CacheEntry { + virtual ~CacheEntry() = default; + }; + + template + struct CacheValue : CacheEntry { + explicit CacheValue(T value) + : value_(std::move(value)) + { } + + const T& value() const { return value_; } + + private: + T value_; + }; + +public: + template + bool contains(const void* key) const { + return cache_.find(key) != cache_.end(); + } + + template + T get(const void* key) const { + return static_cast*>(cache_.at(key).get())->value(); + } + + template + void set(const void* key, T value) { + cache_[key] = std::make_unique>(std::move(value)); + } + + void clear() { cache_.clear(); } + +private: + std::unordered_map> cache_; +}; + template class Container; template struct Context { inversify::IContainer& container; + ResolutionScope* resolutionScope { nullptr }; }; } diff --git a/include/mosure/resolver.hpp b/include/mosure/resolver.hpp index 5ba5211..6538b9c 100644 --- a/include/mosure/resolver.hpp +++ b/include/mosure/resolver.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #ifdef INVERSIFY_BINDING_INSPECTION #include @@ -268,4 +269,52 @@ static_assert( ResolverPtr parent_; }; +template < + typename T, + typename... SymbolTypes +> +class ResolutionCachedResolver + : public Resolver { + static_assert( + std::is_copy_constructible_v, + "inversify::ResolutionCachedResolver requires a copy constructor. Are you caching a unique_ptr?" + ); + +public: + explicit ResolutionCachedResolver(ResolverPtr parent) + : parent_(std::move(parent)) + { } + + inline T resolve(const inversify::Context& context) override { + if (!context.resolutionScope) { + return parent_->resolve(context); + } + + auto key = static_cast(this); + auto& scope = *context.resolutionScope; + + if (scope.template contains(key)) { + return scope.template get(key); + } + + auto value = parent_->resolve(context); + scope.template set(key, value); + + return value; + } + +#ifdef INVERSIFY_BINDING_INSPECTION + inline virtual std::string getResolverLabel() const override { + return std::string("resolution - ") + parent_->getResolverLabel(); + } + + inline virtual std::string getImplementationLabel() const override { + return parent_->getImplementationLabel(); + } +#endif + +private: + ResolverPtr parent_; +}; + } diff --git a/single_include/mosure/inversify.hpp b/single_include/mosure/inversify.hpp index dc58f30..6564321 100644 --- a/single_include/mosure/inversify.hpp +++ b/single_include/mosure/inversify.hpp @@ -33,7 +33,9 @@ SOFTWARE. #include // runtime_error #include // string #include // make_from_tuple, tuple +#include // optional #include // apply, conjunction_v, disjunction_v, false_type, is_copy_constructable, is_same, true_type +#include // unordered_map // #include @@ -107,12 +109,52 @@ class IContainer { namespace mosure::inversify { +class ResolutionScope { + struct CacheEntry { + virtual ~CacheEntry() = default; + }; + + template + struct CacheValue : CacheEntry { + explicit CacheValue(T value) + : value_(std::move(value)) + { } + + const T& value() const { return value_; } + + private: + T value_; + }; + +public: + template + bool contains(const void* key) const { + return cache_.find(key) != cache_.end(); + } + + template + T get(const void* key) const { + return static_cast*>(cache_.at(key).get())->value(); + } + + template + void set(const void* key, T value) { + cache_[key] = std::make_unique>(std::move(value)); + } + + void clear() { cache_.clear(); } + +private: + std::unordered_map> cache_; +}; + template class Container; template struct Context { inversify::IContainer& container; + ResolutionScope* resolutionScope { nullptr }; }; } @@ -507,6 +549,54 @@ static_assert( ResolverPtr parent_; }; +template < + typename T, + typename... SymbolTypes +> +class ResolutionCachedResolver + : public Resolver { + static_assert( + std::is_copy_constructible_v, + "inversify::ResolutionCachedResolver requires a copy constructor. Are you caching a unique_ptr?" + ); + +public: + explicit ResolutionCachedResolver(ResolverPtr parent) + : parent_(std::move(parent)) + { } + + inline T resolve(const inversify::Context& context) override { + if (!context.resolutionScope) { + return parent_->resolve(context); + } + + auto key = static_cast(this); + auto& scope = *context.resolutionScope; + + if (scope.template contains(key)) { + return scope.template get(key); + } + + auto value = parent_->resolve(context); + scope.template set(key, value); + + return value; + } + +#ifdef INVERSIFY_BINDING_INSPECTION + inline virtual std::string getResolverLabel() const override { + return std::string("resolution - ") + parent_->getResolverLabel(); + } + + inline virtual std::string getImplementationLabel() const override { + return parent_->getImplementationLabel(); + } +#endif + +private: + ResolverPtr parent_; +}; + } // #include @@ -522,6 +612,10 @@ class BindingScope { resolver_ = std::make_shared>(resolver_); } + void inResolutionScope() { + resolver_ = std::make_shared>(resolver_); + } + #ifdef INVERSIFY_BINDING_INSPECTION auto getResolver() const { return resolver_; @@ -629,9 +723,21 @@ class Container "inversify::Container symbol not registered" ); - return std::get< + auto* previousScope = context_.resolutionScope; + std::optional scope; + + if (previousScope == nullptr) { + scope.emplace(); + context_.resolutionScope = &scope.value(); + } + + auto result = std::get< inversify::Binding >(bindings_).resolve(context_); + + context_.resolutionScope = previousScope; + + return result; } private: diff --git a/test/dynamic_resolve.cpp b/test/dynamic_resolve.cpp index 45da108..a52fe9f 100644 --- a/test/dynamic_resolve.cpp +++ b/test/dynamic_resolve.cpp @@ -1,5 +1,6 @@ -#include -#include +#include +#include +#include #define CATCH_CONFIG_ENABLE_BENCHMARKING #include @@ -10,7 +11,37 @@ #include "mock/symbols.hpp" -namespace inversify = mosure::inversify; +namespace inversify = mosure::inversify; + +struct ResolutionDependency { + explicit ResolutionDependency(int value) : value(value) { } + + int value; +}; +using ResolutionDependencyPtr = std::shared_ptr; + +struct ResolutionConsumer { + ResolutionConsumer(ResolutionDependencyPtr first, ResolutionDependencyPtr second) + : first(std::move(first)), second(std::move(second)) + { } + + ResolutionDependencyPtr first; + ResolutionDependencyPtr second; +}; +using ResolutionConsumerPtr = std::shared_ptr; + +namespace resolution_symbols { + using dependency = inversify::Symbol; + using consumer = inversify::Symbol; +} + +template <> +struct inversify::Injectable + : inversify::Inject< + resolution_symbols::dependency, + resolution_symbols::dependency + > +{ }; SCENARIO("container resolves dynamic values", "[resolve]") { @@ -126,35 +157,73 @@ SCENARIO("container resolves dynamic values", "[resolve]") { } } - GIVEN("A container with resolution dynamic binding") { - inversify::Container< - symbols::foo, - symbols::bar, - symbols::fizz, - symbols::fizzFactory - > container; - - container.bind().toConstantValue(10); - container.bind().toConstantValue(1.618); - - container.bind().toDynamicValue( - [](auto& ctx) { - auto foo = ctx.container.template get(); - auto bar = ctx.container.template get(); - - auto fizz = std::make_unique(foo, bar); - - return fizz; - } - ); - - WHEN("multiple dependencies are resolved") { - auto fizz1 = container.get(); - auto fizz2 = container.get(); - - THEN("dependencies are unique") { - REQUIRE(fizz1 != fizz2); - } - } - } -} + GIVEN("A container with resolution dynamic binding") { + inversify::Container< + symbols::foo, + symbols::bar, + symbols::fizz, + symbols::fizzFactory + > container; + + container.bind().toConstantValue(10); + container.bind().toConstantValue(1.618); + + container.bind().toDynamicValue( + [](auto& ctx) { + auto foo = ctx.container.template get(); + auto bar = ctx.container.template get(); + + auto fizz = std::make_unique(foo, bar); + + return fizz; + } + ); + + WHEN("multiple dependencies are resolved") { + auto fizz1 = container.get(); + auto fizz2 = container.get(); + + THEN("dependencies are unique") { + REQUIRE(fizz1 != fizz2); + } + } + } + + GIVEN("A container with a resolution scoped dynamic binding") { + inversify::Container< + symbols::foo, + resolution_symbols::dependency, + resolution_symbols::consumer + > container; + + container.bind().toConstantValue(10); + + int factoryCount = 0; + container.bind().toDynamicValue( + [&](auto& ctx) { + ++factoryCount; + auto foo = ctx.container.template get(); + + return std::make_shared(foo); + } + ).inResolutionScope(); + + container.bind().to(); + + WHEN("a graph resolves the dependency twice") { + auto consumer = container.get(); + + THEN("the same dependency instance is reused within the graph") { + REQUIRE(consumer->first == consumer->second); + REQUIRE(factoryCount == 1); + + AND_THEN("a new graph receives a new instance") { + auto next = container.get(); + + REQUIRE(next->first != consumer->first); + REQUIRE(factoryCount == 2); + } + } + } + } +} diff --git a/test/speed.cpp b/test/speed.cpp index c97fd8e..cc5b744 100644 --- a/test/speed.cpp +++ b/test/speed.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #define CATCH_CONFIG_ENABLE_BENCHMARKING #include @@ -13,6 +14,28 @@ namespace inversify = mosure::inversify; +struct ScopedAggregator { + ScopedAggregator(ServiceAPtr first, ServiceAPtr second) + : first_(std::move(first)), second_(std::move(second)) + { } + + ServiceAPtr first_; + ServiceAPtr second_; +}; +using ScopedAggregatorPtr = std::shared_ptr; + +namespace speed_symbols { + using aggregator = inversify::Symbol; +} + +template <> +struct inversify::Injectable + : inversify::Inject< + symbols::symbolA, + symbols::symbolA + > +{ }; + SCENARIO("container resolves automatic values quickly", "[performance]") { GIVEN("A container with an auto binding chain of size 3") { @@ -42,4 +65,20 @@ SCENARIO("container resolves automatic values quickly", "[performance]") { ); }; } + + GIVEN("A container with resolution scoped reuse within a graph") { + inversify::Container< + symbols::foo, + symbols::symbolA, + speed_symbols::aggregator + > container; + + container.bind().toConstantValue(10); + container.bind().to().inResolutionScope(); + container.bind().to(); + + BENCHMARK("resolution scoped aggregator") { + return container.get(); + }; + } }