gracicot / kangaru

🦘 A dependency injection container for C++11, C++14 and later
MIT License
494 stars 39 forks source link

Feature: Return dependency as const when needed #110

Open abeimler opened 1 year ago

abeimler commented 1 year ago

Is your feature request related to a problem? Please describe. I'm trying to make a (const) getter with return m_container.service<di::RegistryService>(), but I get an Error: error: no matching function for call to ‘kgr::container::service<di::RegistryService>() const’

_I also try something like this: return m_container.service<const di::RegistryService>()_

Describe the solution you'd like make a kgr::container::service() const method

Describe alternatives you've considered Right now I'm trying this as a workaround: return std::as_const(m_container.service<di::RegistryService>())

Additional context


  [[nodiscard]] engine::Registry& registry() {
    return m_container.service<di::RegistryService>();
  }
  [[nodiscard]] const engine::Registry& registry() const {
    return m_container.service<di::RegistryService>();
  }

  [[nodiscard]] float getFrameTime() const {
    if (registry().ctx().contains<const GameContext>()) { // registry() needs to be const
      const auto& game_ctx = registry().ctx().get<const GameContext>();
      // ... 
    }
    return 0;
  }
gracicot commented 1 year ago

A const service function would only be possible with supplied service and abstract service that has no default. Any other service could mutate the container to add itself to the list of single service. I'm not sure how I feel overloading the service function that would work only with some service types.

If I enable const service function for all services, I would have no other choice but to throw if a service is not found. This is basically the same problem as with the std::map operator[] function.

In the next version in the 4.x.y series I could add some kind of find function in the container that would return either null or the instance of the service, but I'm not sure when that would be as I'm busy with the 5.x.y series. A MR would certainly help.


There are some workaround possible in the meantime, but none are perfect.

The first one is that if you know in advance that the service is already there, you could just const cast away the const

[[nodiscard]] const engine::Registry& registry() const {
    if (m_container.contains<di::RegistryService>()) {
        return const_cast<kgr::container&>(m_container).service<di::RegistryService>();
    } else {
        throw something;
    }
}

If you don't want to const cast, you can fork the container then call the service function on the fork. If there is single services instantiated by the service call, it's going to be dropped with the temporary. If the service is already in the container there would be no problem.

[[nodiscard]] const engine::Registry& registry() const {
    if (m_container.contains<di::RegistryService>()) {
        return m_container.fork().service<di::RegistryService>();
    } else {
        throw something;
    }
}

The only problem is that if you end up creating services on the fork, you'll get nasty lifetime problems since the fork died. Also forking allocates (a little bit)

There are other workaround but those would ask to change your structure, which you may avoid. I'm writing those so also all future readers would know those solution exist.

Another way would be to just send it as parameter to getFrameTime instead of that function calling the container directly:

  [[nodiscard]] float getFrameTime(engine::Registry const& game_ctx) const {
      // ... 
  }

// elsewhere, call it with the container:

// have a mutable container here, then call the const function
container.invoke([](engine::Registry const& game_ctx) {
    return getFrameTime(game_ctx);
});

Or just contain the engine::Registry& object in the type that has the getFrameTime member function if it's indeed a member.

Let me know if this helps!

abeimler commented 1 year ago

Also thought about const_cast, but that's just ugly.

I now just added an engine::Registry& m_registry as member and init it in the ctor


  Game() : m_registry{m_container.service<di::GameRegistryService>()},
           m_dispatcher{m_container.service<di::GameDispatcherService>()},
           ... {}