-
Notifications
You must be signed in to change notification settings - Fork 199
Quick reference
This page is a quick reference for Fruit features, it's intended for people that already know the basics of Fruit. If you're just starting to learn Fruit, read the tutorial first.
- Constructor injection
- Bindings
- Multibindings
- Providers
- Binding instances
- Annotated injection
- Factories and assisted injection
- Type of a component, components with requirements and automatic bindings
- Injector
- Normalized components
- Injection scopes
- Eager injection
- Lazy injection
- Templated and parametrized components
- Installing multiple components with a pack-expansion expression
- Component replacements
The simplest way to define that a class should be injected using a constructor is to wrap the prototype of that constructor in the class definition with the INJECT macro.
class GreeterImpl : public Greeter {
private:
Writer* writer;
public:
INJECT(GreeterImpl(Writer* writer))
: writer(writer) {
}
// ...
};
The constructor can then be defined inside the class definition (as in the snippet above) or just declared there and
defined out of the class (for example, in a .cpp
file if the class definition is in a header file). If the constructor
is defined elsewhere, only the prototype in the class definition should be wrapped in INJECT()
, the constructor
definition shouldn't be.
Note that where constructor injection is desired, even 0-argument constructors must be explicitly registered, whether or
not they are user-defined. Fruit never uses a constructor unless that constructor has been explicitly marked as the one
that should be used for injection. This is to avoid undesired constructor injection of value types; e.g. if
std::vector<SomeType>
is not bound it's likely that the programmer forgot and Fruit reports an error if this binding
is missing, rather than silently constructing an empty vector.
class StdoutWriter : public Writer {
public:
INJECT(StdoutWriter()) = default;
// ...
};
The INJECT macro can only be used when the constructor has a simple signature. It can't be used with any of the following:
- An explicit
inline
specifier at the point of declaration of the constructor. In this case, the specifier could be removed since for constructors defined in the class definition it's implicit, while for constructors defined outside the class it's theinline
specifier at the point of definition that counts. However if it's present it interferes with theINJECT()
macro. - Templated constructors. In this case, you should use
registerProvider
instead. - Constructors with default values for parameters. In this case, you should use an
Inject
typedef orregisterConstructor
instead.
Note that the restriction on templates is only when the constructor itself is templated. INJECT()
can be used for
templated classes, as long as the constructor is not also templated.
template <typename W>
class GreeterImpl : public Greeter {
private:
W* writer;
public:
INJECT(GreeterImpl(W* writer))
: writer(writer) {
}
// ...
};
The INJECT()
macro can also be used when only some of the constructor parameters should be injected, see
assisted injection for more details.
When the INJECT()
macro can't be used (or as an alternative to avoid using macros) an Inject
typedef can be defined
inside the class instead, with the constructor signature. The two examples in the INJECT section can be rewritten as:
class GreeterImpl : public Greeter {
private:
Writer* writer;
public:
GreeterImpl(Writer* writer)
: writer(writer) {
}
using Inject = GreeterImpl(Writer*);
// ...
};
class StdoutWriter : public Writer {
public:
StdoutWriter() = default; // Optional if no other constructors are defined
using Inject = StdoutWriter();
// ...
};
And these are two examples that couldn't be written using the INJECT()
macro:
class StdoutWriter : public Writer {
public:
StdoutWriter(const char* line_terminator = "\n");
using Inject = StdoutWriter();
// ...
};
class GreeterImpl : public Greeter {
private:
Writer* writer;
public:
template <typename W>
GreeterImpl(W* writer)
: writer(writer) {
}
template <typename W>
GreeterImpl(std::unique_ptr<W> writer)
: writer(writer.get()) {
}
using Inject = GreeterImpl(Writer*);
// ...
};
In the last example the first templated constructor will be used, with W
=Writer
. The usual overloading resolution
(with template parameter deduction) is performed to determine the constructor to use for injection.
As the INJECT
macro, the Inject
typedef can also be used when only some of the constructor parameters should be
injected, see assisted injection for more details.
While the Inject
typedef doesn't have some of the limitations of the INJECT()
macro, there are some situations where
even the typedef is not flexible enough:
- If you can't modify the class definition. This is typically because the class is a third-party class that is not under our control, and the third-party library doesn't use Fruit. Also, a similar situation would occur if you're trying Fruit in an existing codebase and you want to keep Fruit-specific code separate from the existing code.
- If the class should be injected using that constructor only in some cases. For example, in other cases you want a different constructor for that class, or you want to use a provider instead of constructor injection.
The solution here is the registerConstructor()
method available when constructing a component.
class GreeterImpl : public Greeter {
private:
Writer* writer;
public:
GreeterImpl(Writer* writer)
: writer(writer) {
}
// ...
};
fruit::Component<fruit::Required<Writer>, Greeter> getGreeterComponent() {
return fruit::createComponent()
.registerConstructor<GreeterImpl(Writer*)>()
.bind<Greeter, GreeterImpl>();
}
The registerConstructor()
alone can be modelled as:
If only some of the constructor parameters should be injected, see the section on assisted injection.
If none of the constructors does the desired initializations, see the section on providers to see how to bind a lambda function instead.
bind<Interface, Impl>()
specifies that Interface
can be injected from an object of type Impl
. Impl
must be a
class derived from Interface
.
It can also bind factories, i.e. bind std::function<std::unique_ptr<Interface>(T1,...,Tn)>
to
std::function<std::unique_ptr<Impl>(T1,...,Tn)>
or std::function<Impl(T1,...,Tn)>
, for any parameter types
T1,...,Tn
.
See the section on factories and assisted injection to see a
convenient way how to bind a std::function<std::unique_ptr<Impl>(T1,...,Tn)>
in the first place.
Multibindings are a feature of Fruit (inspired by Guice) that allows to bind the same type multiple times, in different ways. They can be used to bind plugins, listeners, etc.
Also, since there are no constraints to the number of bindings (even 0 is allowed), they can be used for optional injection: «if T is bound, then get an instance of T and ...», however in this case T must be bound as a multibinding and not as a normal binding. See lazy injection instead if you want something like «if (...) then inject T» with T always bound but expensive to inject.
The multibindings for a given type are completely separate from the normal bindings; in most cases a type will have either multibindings or bindings, but not both.
Multibindings can depend on normal bindings (e.g. using constructor injection) but they can't depend on other multibindings.
Unlike normal bindings, multibindings do not contribute to a Component type. E.g. a component with multibindings for
Foo and a binding for Bar will have type fruit::Component<Bar>
, not fruit::Component<Foo, Bar>
. This allows to add
multibindings for any type in any component without exposing this in the component type.
Unlike normal bindings, Fruit will not attempt to remove duplicate multibindings, even if they are "the same multibinding".
Example usage:
fruit::Component<> getListenerComponent() {
return fruit::createComponent()
.addMultibinding<Listener, AListener>()
.addMultibinding<Listener, SomeOtherListener>();
}
const std::vector<Listener*>& listeners = injector.getMultibindings<Listener>();
for (Listener* listener : listeners) {
listener->notify();
}
Note that here we specify all multibindings in a single Component
, but this is not necessary; as for normal bindings,
Fruit will collect the multibindings in all components used to create the injector.
This section is about registering providers. For fruit::Provider<T>
, see the section on
lazy injection.
The simplest way to register a class for injection is using constructor injection. However, sometimes there is no constructor that does the desired initialization, and it's not possible to add one (e.g for third-party classes) or it's not reasonable to add one in that class (e.g. for value types, or if the constructor would have to do a too-complex task). In these cases, Fruit allows to construct the object using an arbitrary lambda function.
class Database {
public:
static Database* open(std::string db_name, DbConnectionPool* pool);
void setConnectionTimeout(int seconds);
...
};
fruit::Component<Database> getDatabaseComponent() {
return fruit::createComponent()
... // Bind DbConnectionPool here
.registerProvider([](DbConnectionPool* pool) {
Database* db = Database::open("MyDb", pool);
db->setConnectionTimeout(12);
return db;
});
}
If the lambda returns a pointer, as in the example above, Fruit will take ownership of that pointer and will call
delete
on that object when the injector is destroyed. If you don't want Fruit to take ownership, wrap the pointer in a
holder struct or tuple (but note that you'll need to change the places where that type is used) or consider binding an
instance.
Instead, if the lambda returns a value, Fruit will move the value into the injector and then call the class destructor when the injector is destroyed. Note that this saves a memory allocation/deallocation, so it's preferable to return a value if possible.
registerProvider()
supports only lambdas with no captured variables (aka stateless lambdas). Plain functions are not
supported (but you can wrap the function in a lambda).
If you would like to bind a lambda with captures or a user-defined functor, it can still be done with a workaround.
To use this user-defined functor:
struct Functor {
Functor(int n) {...}
MyClass operator()(Foo* foo) {...}
};
Instead of:
Component<MyClass> getMyClassComponent() {
static const Functor aFunctor(42);
return fruit::createComponent()
... // Bind Foo
// Not allowed !!
.registerProvider([=aFunctor](Foo* foo) { return aFunctor(foo); });
}
Write:
Component<MyClass> getMyClassComponent() {
static const Functor aFunctor(42);
return fruit::createComponent()
... // Bind Foo
.bindInstance(aFunctor)
.registerProvider([](Functor functor, Foo* foo) { return functor(foo); });
}
In most cases, it's easier and less bug-prone to let Fruit manage the lifetime of injected objects. However, in some cases it's necessary to give Fruit access to some object external to the injector, and for which Fruit has no ownership.
Binding instances is also a way to bind values that are constant during the lifetime of the injector, but vary between injectors; see the section on normalized components.
fruit::Component<Foo> getFooComponent() {
static Foo foo = createFoo();
return fruit::createComponent()
.bindInstance(foo);
}
It's also possible to bind constant values, however they then need to be marked as const
in the Component
type too:
fruit::Component<const Foo> getFooComponent() {
const static Foo foo = createFoo();
return fruit::createComponent()
.bindInstance(foo);
}
The caller of bindInstance
must ensure that the provided reference is valid for the entire lifetime of the component and of any components or injectors that install this component. Note that bindInstance
does not create a copy of the parameter.
Unlike other injected objects Fruit will not take ownership of the object passed to bindInstance
, so it won't destroy that object. The typical use-case is to pass a static variable or an object constructed in main
(or similar) that outlives all injectors in the program.
Similar to other Fruit APIs, you can use bindInstance
to bind an annotated value: the syntax is .bindInstance<fruit::Annotated<MyAnnotation, MyType>>(value)
.
There are cases when several implementation types might have the same "natural interface" they implement, except that only one of these types can be bound to it within a single injector. Annotated injection allows to have multiple bindings for the same type and to specify which binding is desired at each point of use. See the tutorial page for a complete example.
To bind a type using annotated injection, use fruit::Annotated<MarkerType, T>
instead of T
, where MarkerType
is
any type you want to use to mark this binding for T
(typically MarkerType
is defined as an empty struct, for
simplicity).
Example binding:
class MainBrakeImpl : public Brake {
public:
INJECT(MainBrakeImpl()) = default;
// ...
};
fruit::Component<fruit::Annotated<MainBrake, Brake>> getMainBrakeComponent() {
return fruit::createComponent()
.bind<fruit::Annotated<MainBrake, Brake>, MainBrakeImpl>();
}
Example use:
class CarImpl : public Car {
private:
Brake* mainBrake;
public:
INJECT(CarImpl(ANNOTATED(MainBrake, Brake*) mainBrake)
: mainBrake(mainBrake) {
}
// Or:
// using Inject = CarImpl(fruit::Annotated<MainBrake, Brake*>);
// CarImpl(Brake* mainBrake)
// : mainBrake(mainBrake) {
// }
// ...
};
Note that the ANNOTATED
macro is only meant to be used within INJECT
. Everywhere else you should write
fruit::Annotated<...>
instead, both to define bindings that use annotated injection and to use them.
Fruit supports annotated types instead of plain types everywhere it would make sense to use them (e.g. you can use
ASSISTED
and ANNOTATED
parameters in the same constructor, you can use an annotated type in bindInstance
).
The only notable place where you should not use annotated types is to annotate the class type in a constructor:
class FooImpl : public Foo {
private:
Bar* bar;
public:
// Error, no need to use ANNOTATED here
INJECT(ANNOTATED(MyAnnotation, FooImpl)(Bar* bar))
: bar(bar) {
}
};
When auto-injecting a class, Fruit already knows the correct annotation, so providing the annotation here would be redundant.
Another thing to note when using annotated injection is that if you bind an implementation class to an interface using 2 different annotations there will be only one instance of the implementation class:
class FooImpl : public Foo {
public:
INJECT(Foo()) = default;
};
struct First {};
struct Second {};
Component<fruit::Annotated<First, Foo>, fruit::Annotated<Second, Foo>>
getFooComponent() {
return fruit::createComponent()
.bind<fruit::Annotated<First, Foo>, FooImpl>()
.bind<fruit::Annotated<Second, Foo>, FooImpl>(); // Allowed, but misleading!
}
That's because there is only one instance of every type in an injector (unless that type is annotated with multiple
annotations, but FooImpl
isn't). This is probably not what you want. In this case, you should also annotate the two
implementation classes, so that the injector will contain a different instance for each annotation:
class FooImpl : public Foo {
public:
INJECT(Foo()) = default;
};
struct First {};
struct Second {};
Component<fruit::Annotated<First, Foo>, fruit::Annotated<Second, Foo>>
getFooComponent() {
return fruit::createComponent()
.bind<fruit::Annotated<First, Foo>, fruit::Annotated<First, FooImpl>>()
.bind<fruit::Annotated<Second, Foo>, fruit::Annotated<Second, FooImpl>>();
}
Note that we did not change the definition of FooImpl
, we only need to specify the annotation in the bind()
calls.
Fruit will then auto-inject fruit::Annotated<First, FooImpl>
and fruit::Annotated<Second, FooImpl>
, both using
FooImpl
's constructor.
There are cases when within a single injector, multiple instances of a given class need to be created, either by
repeatedly calling a single factory function (e.g. for threads in a thread pool) or because there is no single "correct"
instance (different instances are required in different places). Instead of injecting a type MyClass
, a
std::function<std::unique_ptr<MyClass>()>
or std::function<std::unique_ptr<MyClass>(T1,...,Tn)>
can be injected
instead.
Example factory binding:
class MyClass {
public:
MyClass(Foo* foo, int n);
...
};
Component<std::function<std::unique_ptr<MyClass>(int)>> getMyClassComponent() {
return fruit::createComponent()
... // Bind Foo
.registerFactory<std::unique_ptr<MyClass>(Foo*, fruit::Assisted<int>)>(
[](Foo* foo, int n) {
return std::unique_ptr<MyClass>(new MyClass(foo, n));
});
}
Example use of the binding from the injector:
std::function<std::unique_ptr<MyClass>(int)> myClassFactory(injector);
std::unique_ptr<MyClass> x = myClassFactory(15);
Example use of the binding to inject another class:
class Bar {
public:
INJECT(Bar(std::function<std::unique_ptr<MyClass>(int)> myClassFactory)) {
std::unique_ptr<MyClass> x = myClassFactory(15);
...
}
};
Note the fruit::Assisted<>
wrapping the type int
where registerFactory()
is called. This allows to distinguish
types that should be parameters of the factory ("assisted injection", like int
in this example) from ones that should
be injected by Fruit (normal, non-assisted injection, like Foo
in this example).
While Fruit will only create one instance of the factory in each injector, every call to the factory potentially returns
another value; Fruit does not memoize the return value. So two calls to myClassFactory(15)
will return different
instances.
If memoization is desired, it can be performed in the lambda passed to registerFactory()
.
Also, for factories, Fruit does not take ownership of the values created by the factory. So it's preferable to
return a std::unique_ptr
in the lambda instead of a plain pointer to give ownership of the object to the caller of the
factory.
It's possible to bind factories using bind<>
, see the section on bind for more info.
In simple cases, where the lambda passed to registerFactory()
just calls std::unique_ptr<MyClass>(new MyClass(...))
(as in the example above) the INJECT()
macro or an Inject
typedef can be used in the created class itself, instead
of writing the registerFactory()
call explicitly in the component definition.
class MyClass {
public:
INJECT(MyClass(Foo* foo, ASSISTED(int) n));
...
};
Note that when using INJECT()
for assisted injection, the assisted types are wrapped with the ASSISTED()
macro
instead of fruit::Assisted<>
.
class MyClass {
public:
MyClass(Foo* foo, int n);
using Inject = MyClass(Foo*, fruit::Assisted<int>);
...
};
Instead, if an Inject
typedef is used, the types that require assisted injection should be wrapped in
fruit::Assisted<>
(as in the original registerFactory()
call) instead of ASSISTED()
.
In both cases, the constructor definition (in case the constructor is defined outside the class definition) does not
need to wrap the types in any way; this is obvious when using the Inject
typedef, but less obvious when using the
ASSISTED()
macro.
If you would like to inject a factory but do not need to supply any additional assisted arguments, you can create a component that supplies a std::function<std::unique_ptr<MyClass>()>
by just binding the implementation, for example:
using MyClassFactory = std::function<std::unique_ptr<MyClass>();
fruit::Component<MyClassFactory> getMyClassFactoryComponent() {
return fruit::createComponent().bind<MyClass, MyClassImpl>();
}
When using Fruit, every component has a type that declares what type bindings the component exposes and which ones (if any) the component requires.
For example, if a component has type fruit::Component<fruit::Required<T1, T2, T3>, U1, U2>
, that component exposes the
types U1
and U2
but requires bindings for T1
, T2
and T3
. In the common case where a component has no
requirements, the fruit::Required<...>
part can be omitted, so we would have just fruit::Component<U1, U2>
in this
case.
When combining components, requirements can be satisfied with a matching binding, or by installing a component that provides those types.
For example:
fruit::Component<fruit::Required<T1, T2, T3>, U1, U2> getU1U2Component();
fruit::Component<fruit::Required<T2>, T1> getT1Component();
class SomeType : public T2 {
public:
INJECT(SomeType(T3*)) {...}
};
fruit::Component<fruit::Required<T3>, U1> getU1Component() {
return fruit::createComponent()
.install(getU1U2Component) // Now U1,U2 are provided, T1,T2,T3 are required
.install(getT1Component) // Now U1,U2,T1 are provided, T2,T3 are required
.bind<T2, SomeType>(); // Now U1,U2,T1,T2 are provided,
// T3,SomeType are required
}
When Fruit compares the provides/requires with the return type of the getT1Component()
function, it realizes that all
types declared to be provided are already provided, but the type SomeType
is currently required even though it's not
declared as required in the resulting Component. At this point Fruit looks for an Inject
typedef in SomeType (the
INJECT()
macro is just a shorthand for an Inject
typedef), finds it and adds the binding to the component (automatic
binding). Note that in order to add the automatic binding Fruit needs to look at the class definition of SomeType
.
Instead, for other types (e.g. T3
) Fruit doesn't require the definition, a forward declaration is enough to make the
above function compile.
When calculating the required and provided types, Fruit also ensures that there are no dependency loops between bindings and/or components. For example, the following code produces a (compile time) dependency loop error:
fruit::Component<fruit::Required<T2>, T1> getT1Component();
fruit::Component<fruit::Required<T1>, T2> getT2Component();
fruit::Component<T1, T2> getT1T2Component() {
return fruit::createComponent()
.install(getT1Component)
.install(getT2Component); // Error, dependency loop.
}
A Fruit injector is an object of type fruit::Injector<T1,...,Tn>
. Unlike components, injectors can't have
requirements. The types T1,...Tn
are the types exposed by the injector, i.e. the types that can be used in calls to
get<>
. Even though the injector most likely contains bindings for other types, only the types explicitly declared in
the injector type can be directly obtained by the injector itself. Other types will be injected as needed using the
bindings in the injector, but behind the scenes.
Multibindings behave differently, see the section on multibindings for more information.
For each type T
exposed by the injector, the following get()
calls are allowed:
injector.get<T>()
injector.get<T*>()
injector.get<T&>()
injector.get<const T*>()
injector.get<const T&>()
injector.get<std::shared_ptr<T>>()
injector.get<fruit::Provider<T>>()
injector.get<fruit::Provider<const T>>()
See the section on lazy injection to see what fruit::Provider
is; the other types
should be clear.
For constant bindings (e.g. the T
in fruit::Injector<const T>
) only the following get()
calls are allowed:
injector.get<T>()
injector.get<const T*>()
injector.get<const T&>()
injector.get<fruit::Provider<const T>>()
Note that we don't mention any std::function
here. This is because the injector treats std::function
as any other
type.
If T
is std::function<std::unique_ptr<X>(T1,...Tn)>
the above get calls are allowed as for any other T
. Note that
those are allowed for the std::function
type, but not for X
itself.
For convenience, the injector also has a conversion operator to all types that can be retrieved from it using get()
.
For example, this get call:
MyClass* myClass = injector.get<MyClass*>();
can be abbreviated as:
MyClass* myClass(injector);
Normalized components are a feature of Fruit that allows for fast creation of multiple injectors that share most (or all) the bindings.
This is an advanced feature of Fruit that allows to reduce injection time in some cases; if you're just starting to use Fruit you might want to ignore this for now (just construct an Injector from your root Component function).
Using a NormalizedComponent only helps if:
-
You create multiple injectors during the lifetime of a process. E.g. if you only create one injector at startup you won't benefit from using
NormalizedComponent
. -
Some of those injectors share all (or almost all) their bindings.
When both of those requirements apply, you can switch to using NormalizedComponent
in the "similar" injectors by
first refactoring the injectors' root components to be of the form:
fruit::Component<...> getRootComponent(...) {
return fruit::createComponent()
// This contains the bindings common to the group of similar injectors.
.install(getSharedComponent, ...)
// This contains the bindings specific to this injector.
.install(getSpecificComponent, ...);
}
Then you can change your injector construction from:
fruit::Injector<...> injector(getRootComponent, ...);
To:
fruit::NormalizedComponent<fruit::Required<...>, ...> normalized_component(getSharedComponent, ...);
fruit::Injector<...> injector(normalized_component, getSpecificComponent, ...);
This splits the work of constructing the Injector in two phases: normalization (where Fruit will call the Component
functions to collect all the bindings and check for some classes of runtime errors) and the actual creation of the
injector, during which Fruit will also collect/check the additional bindings specific to that injector.
Then you can share the same normalized_component object across all those injectors (also in different threads,
NormalizedComponent
is thread-safe), so that the normalization step only occurs once (i.e., you should only construct
NormalizedComponent
from getSharedComponent
once, otherwise you'd pay the normalization cost multiple times).
Creating an Injector
from a NormalizedComponent
and injecting separate instances is very cheap, on the order of 2 us
for an injection graph with 100 interfaces, 100 implementation classes and 900 edges (for more details see
the Benchmarks page).
This might (depending of course on your performance requirements) allow you to create injectors where it would otherwise be unthinkable, e.g. creating a separate injector for each request in a server.
Injectors that share the same NormalizedComponent
are still independent; for example, if you call
injector.get<Foo>()
in two injectors, each injector will construct its own instance of Foo
.
Example usage:
fruit::Component<fruit::Required<Y>, X> getXComponent(); // Lots of bindings here
int main() {
fruit::NormalizedComponent<fruit::Required<Y>, X> normalizedComponent(
getXComponent());
for (...) {
fruit::Component<Y> yComponent = ...; // Few injector-specific bindings
Injector<X> injector(normalizedComponent, yComponent);
X* x = injector.get<X*>();
...
}
}
Constructing an injector from a NormalizedComponent
is much faster than constructing it from a Component, because most
of the processing (aka normalization) has been done in advance when the Component
object was converted to
NormalizedComponent
. It's also possible to specify some additional bindings at the point where the injector is
created; however, this should be limited to very few bindings (<5 is best, <10 is still ok). If too many of these
bindings are added, the injector construction can become even slower than it would have been without using the
normalized component.
As mentioned before, normalized components are best suited if the vast majority of the bindings are common. A typical use case is in a server, where a separate injector is created for every request, but the bindings of the handler classes are common to all requests; in that case a normalized component can be used, and the request itself (and possibly some metadata about the request) can be added to each injector at the point of construction. See the section on injection scopes for a more detailed example.
This section is meant for readers familiar with Guice. If you don't know what injection scopes are, feel free to skip this section.
Unlike Guice, Fruit does not have injection scopes. However, Fruit's normalized components can be used in situations where with Guice you would have used injection scopes.
As an example, let's consider a situation in which there are 3 scopes, with the types X
, Y
and Z
part of the three
scopes respectively:
// Outer scope
class X {
public:
INJECT(X()) = default;
...
};
fruit::Component<X> getXComponent() {
return fruit::createComponent();
}
// Middle scope
class Y {
public:
INJECT(Y(X* x));
...
};
fruit::Component<fruit::Required<X>, Y> getYComponent() {
return fruit::createComponent();
}
// Inner scope
class Z {
public:
INJECT(Z(X* x, Y* y));
...
};
fruit::Component<fruit::Required<X, Y>, Z> getZComponent() {
return fruit::createComponent();
}
Then the code that creates the injectors can be written as:
fruit::Component<> getEmptyComponent() {
return fruit::createComponent();
}
fruit::Component<X> getXInstanceComponent(X* x) {
return fruit::createComponent()
.bindInstance(*x);
}
fruit::Component<X, Y> getXYInstanceComponent(X* x, Y* y) {
return fruit::createComponent()
.bindInstance(*x)
.bindInstance(*y);
}
fruit::NormalizedComponent<X> outerScopeNormalizedComponent(
getXComponent);
fruit::NormalizedComponent<fruit::Required<X>, Y> middleScopeNormalizedComponent(
getYComponent);
fruit::NormalizedComponent<fruit::Required<X, Y>, Z> innerScopeNormalizedComponent(
getZComponent);
for (...) {
// We want to enter the outer scope here.
fruit::Injector<X> outerScopeInjector(
outerScopeNormalizedComponent, getEmptyComponent);
X* x = outerScopeInjector.get<X*>();
for (...) {
// We want to enter the middle scope here.
fruit::Injector<Y> middleScopeInjector(
middleScopeNormalizedComponent, getXInstanceComponent, x);
Y* y = middleScopeInjector.get<Y*>();
for (...) {
// We want to enter the inner scope here.
fruit::Injector<Z> innerScopeInjector(
innerScopeNormalizedComponent, getXYInstanceComponent, x, y);
Z* z = innerScopeInjector.get<Z*>();
...
}
}
}
The instance of X
is owned by outerScopeInjector
so it lives for a full iteration of the outer loop. The instance of
Y
is owned by middleScopeInjector
so it lives for an iteration of the middle loop. Finally, the instance of Z
is
owned by innerScopeInjector
so it only lives for an iteration of the inner loop.
See the section on normalized components and the section on bindInstance if the code above is not clear.
If you create an injector that will be used concurrently by multiple threads, you either need to synchronize the calls
(using a lock) or you might want to inject everything to start with, so that all get
and getMultibindings
calls on
the injector don't need to modify it (as the objects have already been constructed). The Injector
class provides a
method to do this: eagerlyInjectAll()
. After calling this method, the Injector
object can be shared by multiple
threads with no locking.
However, this does not perform lazy injection; so if you're only injecting a Provider<Foo>
, Foo
will not be
constructed. This means that even after calling this method, get()
calls on Provider
objects are still not
thread-safe (unless you're sure that that class has already been injected).
Lazy injection is a feature that can be used when a type Foo
depends on a type Bar
only for some methods, and Bar
is expensive to inject. This may not necessarily be because the Bar
constructor is slow, it might be expensive because
it triggers the injection of many other types. In this case, Foo
can depend on fruit::Provider<Bar>
instead of
directly depending on Bar
. This delays the injection of Bar
(and of the types that need to be injected in order to
inject the Bar
object) until the get
method of the provider is called.
Let's see some example code for this situation:
class Bar {
public:
INJECT(Bar(ReallyExpensiveClass x, SomeOtherReallyExpensiveClass y));
...
};
class Foo {
private:
fruit::Provider<Bar> barProvider;
public:
INJECT(Foo(fruit::Provider<Bar> barProvider))
: barProvider(barProvider) {
}
void doSomething() {
Bar* bar = barProvider.get();
...
}
};
Bar
, ReallyExpensiveClass
and SomeOtherReallyExpensiveClass
are only injected when foo->doSomething()
is called,
not when Foo
itself is injected. This can give a sizable performance win if foo->doSomething()
is not always called.
Every call to get()
(within the same injector) returns the same Bar
instance, and that's also the same instance that
will be used to inject other classes (if there are other classes that depend on Bar
).
This means that the call to get()
contains a branch (basically an if (is_already_initialized) {...}
) so once the
Bar*
has been retrieved you might want to store it in a variable or field instead of calling get()
multiple times.
get*Component()
functions are just normal functions; therefore they can be templated:
template <typename T, typename U>
class FooInterface { ... };
template <typename T>
class FooImpl : public FooInterface<T, std::vector<T>> {
public:
INJECT(FooImpl()) = default;
...
};
template <typename T>
fruit::Component<FooInterface<T, std::vector<T>> getFooComponent() {
return createComponent()
.bind<FooInterface<T, std::vector<T>>, FooImpl<T>>();
}
And they can parametrized:
class FooInterface { ... };
class OldFooImpl : public FooInterface {
public:
INJECT(OldFooImpl()) = default;
...
};
class NewFooImpl : public FooInterface {
public:
INJECT(NewFooImpl()) = default;
...
};
fruit::Component<FooInterface> getFooComponent(bool use_old_impl) {
if (use_old_impl) {
return fruit::createComponent()
.bind<FooInterface, OldFooImpl>();
} else {
return fruit::createComponent()
.bind<FooInterface, NewFooImpl>();
}
}
The last example also shows how to do conditional injection.
When parameterizing them, however, there are some constraints on the argument types. Specifically, they must be:
- Copy-constructible
- Move-constructible
- Assignable
- Move-assignable
- Equality-comparable (i.e.,
operator==
must be defined for two values of that type) - Hashable (i.e.,
std::hash
must be defined for values of that type)
Note that this only applies to the arguments of the function, not the types declared by the returned Component
. In the
example above, this means that bool
must satisfy these requirements (and it does), but FooInterface
, NewFooImpl
and OldFooImpl
don't need to.
Fruit has a installComponentFunctions()
that helps install multiple components created using a template pack-expansion
expression.
This is a very niche feature, it's only useful within a templated get*Component
function that has a variadic
argument. So most users will never need/use this.
Given a templated get*Component
function:
template <typename T>
fruit::Component<T> getDefaultConstructorComponent() {
return fruit::createComponent()
.registerConstructor<T()>();
}
We can install getDefaultConstructorComponent
for multiple different T
s at once, as follows:
template <typename... Ts>
fruit::Component<Ts...> getDefaultConstructorsComponent() {
return fruit::createComponent()
.installComponentFunctions(
fruit::componentFunction(getDefaultConstructorComponent<Ts>)...);
}
Note that we need to wrap each get*Component
function in fruit::componentFunction
. This is to allow
passing arguments to the get*Component
functions if desired (see the previous section for more details).
In some cases (notably in tests) we want to install a component but without some of the components that it installs (directly or indirectly). Component replacements allow to specify these substitutions so that you don't need to split the component or create a modified copy of the component function hierarchy.
For example, if you have these components:
fruit::Component<MyDependency> getDependencyComponent() {...}
fruit::Component<Foo> getFooComponent() {
return fruit::createComponent()
.install(getDependencyComponent)
.bind<Foo, FooImpl>();
}
fruit::Component<Bar> getBarComponent() {
return fruit::createComponent()
.install(getFooComponent)
.bind<Bar, BarImpl>();
}
When you test Bar
, you might want to replace getDependencyComponent
with a component that binds a fake
MyDependency:
fruit::Component<MyDependency> getFakeDependencyComponent() {...}
To do so, you can define a component like:
fruit::Component<Bar> getBarComponentWithFakeDependency() {
return fruit::createComponent()
.replace(getDependencyComponent).with(getFakeDependencyComponent)
.install(getBarComponent);
}
This component is equivalent to:
fruit::Component<Bar> getBarComponentWithFakeDependency() {
return fruit::createComponent()
.install(getFakeDependencyComponent)
.bind<Foo, FooImpl>()
.bind<Bar, BarImpl>();
}
However this way you don't need to duplicate the bindings for Foo
and Bar
, and you don't even need to include them
in the translation unit (i.e., cc/cpp file) that defines getBarComponentWithFakeDependency()
.
In codebases with many layers, this can save a lot of duplication.
Note that the .replace(...).with(...)
must appear before installing the component to which it's applied to;
e.g., in the example above note how we install getBarComponent
after the replacement in
getBarComponentWithFakeDependency
.
If you add a replacement after the replaced component has been installed, Fruit will report an error at run-time.
In the example above, the replaced and replacement component functions had no arguments, however it's also possible
to replace component functions with args. The arguments of the replaced and replacement component functions are
independent; for example .replace(getDependencyComponentWithArgs, 15).with(myFakeComponentWithNoArgs)
is allowed
and it would replace all install(getDependencyComponentWithArgs, 15)
calls with install(myFakeComponentWithNoArgs)
.
The component types returned by the replaced and replacement components must be the same. For example, this is NOT
allowed:
fruit::Component<MyDependency, SomethingElse> getFakeDependencyComponentWithSomethingElse() {...}
fruit::Component<Bar> getBarComponentWithFakeDependency() {
return fruit::createComponent()
.replace(getDependencyComponent).with(getFakeDependencyComponentWithSomethingElse) // error!
.install(getBarComponent);
}
But replacing a replaced component is allowed:
fruit::Component<MyDependency> getOtherFakeDependencyComponent() {...}
fruit::Component<Bar> getBarComponentWithOtherFakeDependency() {
return fruit::createComponent()
// The two replacements can appear in any order, but they must both be before the install().
.replace(getFakeDependencyComponent).with(getOtherFakeDependencyComponent)
.replace(getDependencyComponent).with(getFakeDependencyComponent)
.install(getBarComponent);
}
Of course this is a simple example, in the real world the replacements and the install would probably come from other components. And note that you can also replace components that define replacements, for example:
fruit::Component<> getFakeDependencyReplacementComponent() {
return fruit::createComponent()
.replace(getDependencyComponent).with(getFakeDependencyComponentWithSomethingElse);
}
fruit::Component<...> getComponent() {
return fruit::createComponent()
.replace(getFakeDependencyReplacementComponent).with(...)
.install(...);
}
Replacements are only installed if the replaced component is installed, otherwise they are ignored.
In the first example above, if getFooComponent
didn't install getDependencyComponent
, when a test creates an
injector for getBarComponentWithFakeDependency
it would not install getFakeDependencyComponent
.
For a more complete example, see the Testing chapter of the tutorial.