We use an execution environment. Everyone gets a copy of the environment; on the environment, you can query for other services by name. Either services are created on-demand when asked for, or everyone is first created and put into the environment, before everyone is configured.
The most straightforward way of doing this is to derive all services from IService, and make the environment be a std::map< std::string, IService * >. IService then defines the lifecycle; something like:
class IService;class IEnvironment { public: virtual void setService( char const * name, IService * svc ) = 0; virtual IService * service( char const * name ) = 0;};class IService { public: virtual void setEnvironment( IEnvironment * env ) = 0; virtual void start() = 0;};class IRenderSystem : public IService {};class ITextureManager : public IService {};class ISoundSystem : public IService {};
After you get a specific IService by name, you can typically just static_cast<> down to the specific service interface for that name. You could use some macro-ing, and/or dynamic_cast<> asserts, to make sure it's all safe.
Typically, you'd first create everyone and add them to the environment. Then you'd do a for_each() over the environment, calling setEnvironment() on all the services within the environment. The service implementations, at this point, extract all the other services they need. Last, you do a for_each() over the environment, calling start(); that's the point when a service knows that all services are correctly configured and the system is up and running. Of course, an individual service should be able to start serving before start(), but after setEnvironment(), because other services may call it from within their start() implementations.