# Core Concepts This page covers the parts of Dingo that shape object graph behavior: registration modes, lifetimes, stored forms, arrays, variants, factories, and interface-oriented resolution. For the internal model behind these features, start with the [architecture docs](architecture/README.md). ## Registration Model Dingo does not require managed types to inherit from a framework base class or to expose special metadata. Registration describes how a type should be handled at the registration site, either through runtime calls or compile-time bindings. Common registration policies: - `scope<...>` selects lifetime and caching behavior - `storage<...>` selects the stored representation - `interfaces<...>` exposes the registration through one or more interface types - `factory<...>` overrides construction when constructor deduction is not enough Many registrations stay short because Dingo can infer the missing policy from the rest. The same policies are used by both registration modes: ```c++ container<> runtime_container; runtime_container.register_type, storage>(); using static_bindings = bindings, storage>>; container compile_time_container; ``` Runtime registration is mutable container configuration. Compile-time bindings are part of the container type and can include `dependencies<...>` for static graph checks before the program runs. Static cycles are rejected unless every binding in the cycle uses `scope`. Example code included from [../examples/registration/non_intrusive.cpp](../examples/registration/non_intrusive.cpp): ```c++ container<> container; // Registration of a struct A container.register_type, // using unique scope factory>, // using constructor detection storage>, // stored as // unique_ptr interfaces // resolvable as A >(); // As some policies can be deduced from the others, the above // registration simplified container.register_type, storage>>(); ``` See: - [include/dingo/registration/type_registration.h](../include/dingo/registration/type_registration.h) ## Scopes And Storage Scope and storage work together. Scope controls lifetime; storage controls the representation that Dingo keeps and converts during resolution. ### Scopes - `external`: refer to an already existing object - `unique`: build a fresh instance per resolution - `shared`: cache and reuse the same instance - `shared_cyclical`: cache instances while permitting cyclic graphs ### Storage Forms The stored type can be a value, raw pointer, `std::unique_ptr`, `std::shared_ptr`, `std::optional`, raw arrays, or another supported form. Dingo respects the usual semantics of that form when creating or passing instances around. The injectable result depends on both: - the chosen scope - the chosen stored type - a `shared` registration stored as `std::shared_ptr` can naturally satisfy `T&`, `T*`, and `std::shared_ptr` style dependencies - an `external` registration can expose an already-owned object without moving ownership into the container - some exact wrapper forms intentionally resolve only as the wrapper itself, rather than as every possible contained alternative Example code included from [../examples/storage/scope_external.cpp](../examples/storage/scope_external.cpp): ```c++ struct A {}; A instance; container<> container; // Register existing instance of A, stored as a pointer. container.register_type, storage>(&instance); // Resolution will return an existing instance of A casted to required type. assert(&container.resolve() == container.resolve()); ``` Example code included from [../examples/storage/scope_unique.cpp](../examples/storage/scope_unique.cpp): ```c++ struct A {}; container<> container; // Register struct A with unique scope container.register_type, storage>(); // Resolution will get a unique instance of A container.resolve(); ``` Example code included from [../examples/storage/scope_shared.cpp](../examples/storage/scope_shared.cpp): ```c++ struct A {}; container<> container; // Register struct A with shared scope container.register_type, storage>(); // Resolution will always return the same A instance assert(container.resolve() == &container.resolve()); ``` Example code included from [../examples/storage/scope_shared_cyclical.cpp](../examples/storage/scope_shared_cyclical.cpp): ```c++ // Forward-declare structs that have cyclical dependency struct A; struct B; // Declare struct A, note that its constructor is taking arguments of struct B struct A { A(B &b, std::shared_ptr bptr) : b_(b), bptr_(std::move(bptr)) {} B &b_; std::shared_ptr bptr_; }; // Declare struct B, note that its constructor is taking arguments of struct A struct B { B(A &a, A *aptr) : a_(a), aptr_(aptr) {} A &a_; A *aptr_; }; container<> container; // Register struct A with cyclical scope container.register_type, storage>(); // Register struct B with cyclical scope container.register_type, storage>>(); // Returns instance of A that has correctly set b_ member to an instance of // B, and instance of B has correctly set a_ member to an instance of A. // Conversions are supported with cycles, too. A &a = container.resolve(); B &b = container.resolve(); // Check that the instances are constructed as promised assert(&a.b_ == &b); assert(&a.b_ == a.bptr_.get()); assert(&b.a_ == &a); assert(b.aptr_ == &a); ``` See: - [include/dingo/storage/external.h](../include/dingo/storage/external.h) - [include/dingo/storage/unique.h](../include/dingo/storage/unique.h) - [include/dingo/storage/shared.h](../include/dingo/storage/shared.h) - [include/dingo/storage/shared_cyclical.h](../include/dingo/storage/shared_cyclical.h) ## Arrays Dingo supports raw C++ arrays, `std::unique_ptr`, and `std::shared_ptr`, including N-D array shapes. In practice: - register the exact array shape to store - resolve either the exact shape or the supported borrowed/owning array view for that scope - keep shape in mind, because nested arrays do not flatten to `T*` The main constraints are: - `resolve()` is not supported because arrays are not returned by value - unique array storage can hand out owning handles such as `std::unique_ptr` - shared array storage can hand out stable borrowed views and shared handles Example code included from [../examples/storage/array.cpp](../examples/storage/array.cpp): ```c++ using namespace dingo; struct cell { cell() = default; }; struct row_consumer { cell (*rows)[3]; explicit row_consumer(cell (*init)[3]) : rows(init) {} }; container<> raw_container; // Register a raw N-D array in shared scope. raw_container.register_type, storage>(); // Resolve row view, exact pointer view and inject the row view. auto *rows = raw_container.resolve(); auto &exact = raw_container.resolve(); row_consumer borrowed = raw_container .construct>(); container<> unique_container; // Register a fixed-size array in unique scope and resolve it as an owning // dynamic array handle. unique_container.register_type, storage>(); auto owned = unique_container.resolve>(); container<> shared_container; // Register a shared smart array directly. shared_container.register_type, storage>>( callable([] { return std::shared_ptr(new cell[4]); })); auto shared = shared_container.resolve>(); ``` See: - [include/dingo/type/type_traits.h](../include/dingo/type/type_traits.h) ## Variants Dingo handles variants in two distinct places: - `construct, constructor>()` constructs the variant by selecting `A` as the alternative to build - `register_type<..., storage>, factory<...>>()` lets the container store the variant and later resolve either the whole variant or its currently held alternative The current rules are narrow on purpose: - the selected alternative must appear exactly once in the variant type - the whole variant remains resolvable - uniquely occurring alternatives are also resolvable from variant storage - if a requested alternative is available but the current instance holds a different alternative, resolution fails as `type_not_convertible_exception` - duplicate alternative types are rejected at compile time for direct resolution - unique variant storage resolves the whole variant as a value or rvalue, and a held alternative as a value or rvalue - shared and external variant storage resolve the whole variant as values, references, or pointers, and a held alternative as values, references, or pointers Example code included from [../examples/container/variant.cpp](../examples/container/variant.cpp): ```c++ struct A { explicit A(int init) : value(init) {} int value; }; struct B { explicit B(float init) : value(init) {} float value; }; container<> construct_container; construct_container.register_type, storage>(7); construct_container.register_type, storage>(3.5f); // Construct a variant by selecting which alternative should be built. [[maybe_unused]] auto detected = construct_container.construct, constructor>(); assert(std::holds_alternative(detected)); [[maybe_unused]] auto explicit_ctor = construct_container.construct, constructor>(); assert(std::holds_alternative(explicit_ctor)); container<> unique_container; unique_container.register_type, storage>(); unique_container.register_type, storage>, factory>>(); // Resolve either the whole variant or its currently held alternative. [[maybe_unused]] auto value = unique_container.resolve>(); assert(std::holds_alternative(value)); [[maybe_unused]] auto selected = unique_container.resolve(); try { unique_container.resolve(); } catch (const type_not_convertible_exception &) { } std::variant existing(std::in_place_type, 9); container<> external_container; external_container .register_type, storage &>>(existing); [[maybe_unused]] auto &ref = external_container.resolve &>(); assert(&ref == &existing); assert(std::holds_alternative(ref)); [[maybe_unused]] auto &held = external_container.resolve(); assert(&held == &std::get(existing)); ``` See: - [test/matrix/README.md](../test/matrix/README.md) - [include/dingo/type/type_traits.h](../include/dingo/type/type_traits.h) ## Factories By default, Dingo tries to select a usable constructor automatically. That keeps the common case concise, but explicit construction overrides are available when needed. Factory styles in the repo: - automatic constructor deduction - explicit constructor selection - static function factory - stateful callable factory ### Constructor Deduction The default registration path uses `constructor` from [include/dingo/factory/constructor.h](../include/dingo/factory/constructor.h). That public shorthand delegates to the lower-level `constructor_detection` machinery in [include/dingo/factory/constructor_detection.h](../include/dingo/factory/constructor_detection.h). When a type is registered without an explicit `factory<...>`, Dingo tries to pick a constructor automatically. In practice, the default path is: - try constructor deduction for registered types - use the highest-arity constructor shape that Dingo can prove is constructible - resolve the detected arguments from the container `construct()` uses the same deduction machinery for unmanaged objects. Constructor detection has two result forms: - shape detection reports whether the selected constructor is concrete, generic, or invalid, together with its arity; this is the default used for normal container construction - signature detection starts from that same selected shape and additionally recovers its parameter types as a `type_list`; construction then uses the recovered list, so reported dependencies and injected dependencies cannot diverge The lower-level detection result exposes `kind` and `arity`. A concrete signature result also exposes `arguments`; zero-argument construction produces `type_list<>`, while an unavailable signature produces `void`. Explicit constructor declarations already contain their argument list and therefore do not require signature recovery. ### Constructor-Deduction Limits Constructor deduction is intentionally useful, not magical. The main limits are: - detection is based on compile-time constructibility checks, not on every overload-resolution nuance that handwritten code might express - the detector prefers the highest-arity constructible shape, which is not always the overload to commit to in public code - constructor templates and other generic catch-all constructor shapes are rejected from auto-detection; those types must use `factory>` - ambiguous or unsupported cases should be resolved explicitly with `factory>`, `factory>`, `callable(...)`, or `callable(...)` - `construct()` uses constructor deduction directly, but unregistered `resolve()` only auto-constructs plain types when they are aggregates or explicitly opted in through `is_auto_constructible` - opting a type into auto-construction does not bypass ambiguity checks - auto-construction requires `T` to be complete - borrowed requests such as `resolve()` and `resolve()` can use a forward-declared `T` as long as the type is already registered - constructing another type can still depend on a forward-declared `T&` or `T*`; the dependency is looked up, not auto-constructed - reference-style wrapper values only participate in the reference-based deduction path when the wrapper opts in through `type_traits::is_reference_resolvable` - automatic detection does not perform structural signature recovery when nested wrapper or alternative conversions make a constructor probe ambiguous; provide the exact signature with `factory>` - constructor arity is bounded by `DINGO_CONSTRUCTOR_DETECTION_ARGS` That makes constructor deduction a good default for ordinary classes and aggregates, but not a substitute for an explicit factory when constructor choice is part of the type's contract. Reach for an explicit factory when: - constructor deduction is ambiguous - the type exposes a templated or forwarding constructor - the type should be built through a named factory function - construction depends on extra callable state - selecting a specific alternative for variant construction or storage matters Example code included from [../examples/factory/factory_constructor_deduction.cpp](../examples/factory/factory_constructor_deduction.cpp): ```c++ struct A { A(int); // Definition is not required as constructor is not called A(double, double) {} // Definition is required as constructor is called }; container<> container; container.register_type, storage>(1.1); // Constructor with a highest arity will be used (factory<> is deduced // automatically) container .register_type, storage /*, factory> */>(); ``` Example code included from [../examples/factory/factory_constructor.cpp](../examples/factory/factory_constructor.cpp): ```c++ struct A { A(int); // Definition is not required as constructor is not called A(double) {} // Definition is required as constructor is called }; container<> container; container.register_type, storage>(1.1); // Register A with the explicitly selected A(double) constructor. // Manual disambiguation is required to avoid a compile-time assertion. container.register_type, storage, factory>>(); ``` Templated constructors are supported when the type is registered with an explicit constructor signature: ```c++ struct Generic { template Generic(First, Second) {} }; container.register_type, storage>(7); container.register_type, storage>(3.5f); container.register_type, storage, factory>>(); ``` Example code included from [../examples/factory/factory_function.cpp](../examples/factory/factory_function.cpp): ```c++ // Declare struct A that has an inaccessible constructor struct A { // Provide factory function to construct instance of A static A factory() { return A(); } private: A() = default; }; container<> container; // Register A that will be instantiated by calling A::factory() container .register_type, storage, factory>>(); ``` Example code included from [../examples/factory/factory_callable.cpp](../examples/factory/factory_callable.cpp): ```c++ struct A { int value; }; struct overloaded_factory { A operator()(int value) const { return A{value * 2}; } A operator()(float value) const { return A{static_cast(value) + 100}; } }; container<> container; // Register int that will be passed to the callable below container.register_type, storage>(2); // Register A that will be instantiated by calling provided lambda function // with arguments resolved using the container. container.register_type, storage>(callable([](int value) { return A{value * 2}; })); assert(container.resolve().value == 4); // Explicit signatures also work for overloaded functors. assert(container.construct(callable(overloaded_factory{})).value == 4); ``` See: - [test/factory/constructor_detection.cpp](../test/factory/constructor_detection.cpp) - [test/matrix/README.md](../test/matrix/README.md) - [include/dingo/factory/constructor_detection.h](../include/dingo/factory/constructor_detection.h) - [include/dingo/factory/constructor.h](../include/dingo/factory/constructor.h) - [include/dingo/factory/function.h](../include/dingo/factory/function.h) - [include/dingo/factory/callable.h](../include/dingo/factory/callable.h) ## Interfaces And Service-Locator Style Resolution Registrations can be exposed through base classes or other interface types. The upcast is fixed at registration time, which keeps multiple-inheritance cases predictable without relying on `dynamic_cast` for ordinary resolution. This is the right fit when: - concrete implementations should stay hidden behind an interface - one concrete type should be resolvable through several interfaces Example code included from [../examples/container/service_locator.cpp](../examples/container/service_locator.cpp): ```c++ // Interface that will be resolved struct IA { virtual ~IA() = default; }; // Struct implementing the interface struct A : IA {}; container<> container; // Register struct A, resolvable as interface IA container.register_type, storage, interfaces>(); // Resolve instance A through interface IA IA &instance = container.resolve(); assert(dynamic_cast(&instance)); ``` ## Multibindings And Collections Dingo can register multiple implementations under one interface and then resolve them as a collection. This is a good fit for extension-point style code such as: - processors - handlers - strategy lists The collection can use a standard aggregation rule or a custom one. Example code included from [../examples/index/multibindings.cpp](../examples/index/multibindings.cpp): ```c++ struct IProcessor { virtual ~IProcessor() = default; }; template struct Processor : IProcessor {}; struct container_traits : dynamic_container_traits { using lookup_definition_type = lookups>; }; container container; // Register types under the same interface container.template register_type, storage>, interfaces>(); container.template register_type, storage>, interfaces>(); // Resolve the collection container.template resolve>(); ``` Example code included from [../examples/registration/collection.cpp](../examples/registration/collection.cpp): ```c++ struct ProcessorBase { virtual ~ProcessorBase() = default; virtual void process(const void *) = 0; virtual std::type_index type() = 0; }; template struct Processor : ProcessorBase { virtual std::type_index type() override { return typeid(T); } void process(const void *transaction) override { static_cast(this)->process_impl( *reinterpret_cast(transaction)); } }; struct StringProcessor : Processor { void process_impl([[maybe_unused]] const std::string &value) {}; }; struct VectorIntProcessor : Processor, VectorIntProcessor> { void process_impl([[maybe_unused]] const std::vector &value) {}; }; struct Dispatcher { Dispatcher( std::map> &&processors) : processors_(std::move(processors)) {} Dispatcher(const Dispatcher &) = delete; Dispatcher &operator=(const Dispatcher &) = delete; Dispatcher(Dispatcher &&) = default; Dispatcher &operator=(Dispatcher &&) = default; template void process(const T &value) { processors_.at(typeid(T))->process(&value); } private: std::map> processors_; }; struct traits : dingo::dynamic_container_traits { using lookup_definition_type = dingo::lookups>; }; container container; container .register_type, storage>, interfaces>(); container .register_type, storage>, interfaces>(); container.register_type_collection< scope, storage>>>( [](auto &collection, auto &&value) { collection.emplace(value->type(), std::forward(value)); }); auto dispatcher = container.construct(); dispatcher.process(std::string("")); ``` See: - [include/dingo/registration/collection_traits.h](../include/dingo/registration/collection_traits.h) ## Runtime Model And Error Handling Dingo supports both runtime registrations and compile-time bindings. Runtime registration keeps cross-module usage practical, but wiring errors on that path surface as exceptions during resolution. Compile-time bindings move the declared graph into the type system, so missing static dependencies and unsupported static cycles can be diagnosed at compile time. Container changes are atomic. If a registration or resolution operation throws, the container is restored to the state it had when the operation began. An operation therefore either succeeds completely or leaves the container unmodified; partially installed registrations, cached resolutions, lookup entries, and container-owned objects are rolled back. Across a container hierarchy, each container with runtime state is its own atomic boundary. Parent fallback runs as a parent operation: a successful parent resolution remains committed if later child construction fails, while a failure inside the parent operation rolls the parent back before the exception propagates. This guarantee covers state managed by the container. Dingo cannot undo external side effects performed by user constructors, factories, callbacks, or destructors before they throw. See: - [Advanced Topics](advanced-topics.md)