Tuesday, April 7, 2020

Managing Class Options

Managing Class Options

I’ve found myself recently needing to handle passing and managing options across all of the components of our application.
I wanted all components to have their own options that could be managed externally.

Approach

I nest the options struct in the class and let it be qualified by the class name.

class MyComponent {
public:
  struct Options {
    struct {
      bool some_option{false};
      /* some other options */
    } MyComponent;
  };
  
  MyComponent() {}
  MyComponent(std::shared_ptr<Options>& options) :
      m_options{options} {}
  /* ... */
private:
  std::shared_ptr<Options> m_options{new Options};
};

We will devise a way to aggregate options and get a specific slice of options from it.

template <typename... TOptions>
struct OptionsAggregate : public TOptions... {
  using Type = OptionsAggregate<TOptions...>;
  template <typename TOptionType>
  auto Get() -> TOptionType& {
    static_assert(std::is_base_of_v<TOptionType, Type>,
      "Requested options are not a base of this aggregate");
    return static_cast<TOptionType&>(*this);
  }
};

So this variadic class template will take several options classes and inherit from them. It also has a Get method to get a slice of the aggregate.

We will have a ComponentManager that manages all of the components and their options.

template <typename TOptions>
class ComponentManager {
public:
  ComponentManager() {}
  ComponentManager(std::shared_ptr<MyComponent> component,
                   std::shared_ptr<Options> options) :
     m_options{std::move(options)} {}
  void Run();
private:
  std::shared_ptr<TOptions> m_options{new TOptions};
};

We can then use all of these things like the following:

struct GeneralOptions {
  bool debug{false};
};

using ComponentOptions = OptionsAggregate<GeneralOptions, MyComponent::Options>;
auto options = std::make_shared<ComponentOptions>();

options->Get<GeneralOptions>().debug = true;
/* set other options */

// the options that component can see are only a slice of the overall options
auto component = std::make_shared<MyComponent>(options);

ComponentManager manager{component, options};

A problem I discovered with this method appears when using the slicing syntax from ComponentManager methods which results in very unfortunate syntax. Imagine component manager had a method called Run:

template<typename TOptions>
void ComponentManager<TOptions>::Run() {
  auto& general_options = m_options->template Get<GeneralOptions>();
  /* use general_options */
}

This occurs because m_options depends on a template parameter as well as OptionsAggregate::Get.

Check Compiler Explorer for a full example.