J Reichardt
J Reichardt

Reputation: 167

Policy Based Design - Ideomatic way to deal with multitude of types, e.g. storing them in a container, iterating, etc

In the hello world example of policy based design from wikipedia we use a common interface HelloWorld and configure it with different policies through templates - so far so good:

int main() {
  // Example 1
  typedef HelloWorld<OutputPolicyWriteToCout, LanguagePolicyEnglish>
      HelloWorldEnglish;

  HelloWorldEnglish hello_world;
  hello_world.Run();  // Prints "Hello, World!".

  // Example 2
  // Does the same, but uses another language policy.
  typedef HelloWorld<OutputPolicyWriteToCout, LanguagePolicyGerman>
      HelloWorldGerman;

  HelloWorldGerman hello_world2;
  hello_world2.Run();  // Prints "Hallo Welt!".
}

This is all very nice and elegant, but what is the idiomatic way of managing / storing a collection of such configurable objects? For example, one would like to write

std::vector< some_magic_type > seasons_greetings;  // What is the common type specifying the public interface only?
seasons_greetings.push_back(hello_world);  // which is of type HelloWorldEnglish
seasons_greetings.push_back(hello_world2); // which is of type HelloWorldGerman
for (greeting : seasons_greetings) {
  greeting.Run() // access only the public interface
}

In designing interfaces as base classes and deriving specialised implementations from them, I don't have this problem - I can always store pointers to the base class type - but I need to spell out all implementations leading to a whole lot of derived classes. Policy Based Design promised to alleviate the explosion of derived classes that comes with this by using templates to mix and match behavior. But I pay for this with a whole lot of different types.

There must be an idiomatic way to deal with this. Any insight is greatly appreciated.

P.S. I admit that I did not buy the book, but you might have guessed already. This answer suggests storing a collection implies an inheritance based design, but does it really?

Upvotes: 0

Views: 172

Answers (2)

J Reichardt
J Reichardt

Reputation: 167

So here is a solution I found after pondering the comments again - key seems to combine the best of both worlds, polymorphism and policies - who would have thought ....

By introducing a common base class that defines only the public interface of the behavior class and then deriving the behavior class from that and the policies, I can get exactly what I want:

class HelloWorldInterface {
    public:
    // Behavior method.
    void Run() const {
        run();
    }
    private:
    virtual void run() const = 0;
};

template <typename OutputPolicy, typename LanguagePolicy>
class HelloWorld : public HelloWorldInterface, private OutputPolicy, private LanguagePolicy {
 private:
   using OutputPolicy::Print;
   using LanguagePolicy::Message;
   void run() const override final {
       Print(Message());
   }
};

Then, I can write basically what I wanted from the beginning:

int main() {
  // Example 1
  using HelloWorldEnglish =  HelloWorld<OutputPolicyWriteToCout, LanguagePolicyEnglish>;

  // Example 2
  // Does the same, but uses another language policy.
  using HelloWorldGerman =  HelloWorld<OutputPolicyWriteToCout, LanguagePolicyGerman>;

  HelloWorldEnglish hello_world;
  HelloWorldGerman hello_world2;

  std::vector<const HelloWorldInterface*> greetings{&hello_world, &hello_world2};

  for (auto x : greetings) {
      x -> Run();
  }
}

This way, one gets a single public interface that exposes behaviours resulting from an arbitrary combination of policies. Looks trivial now, though.

Upvotes: 1

Klaus
Klaus

Reputation: 25613

If you want to stay with unrelated classes you can use std:variant in combination with std::visit.

Example ( simply extend the main function from the original example )

    using Variant = std::variant< HelloWorld<OutputPolicyWriteToCout, LanguagePolicyEnglish>, HelloWorld<OutputPolicyWriteToCout, LanguagePolicyGerman> >;
    std::vector< Variant > seasons_greetings; 
    seasons_greetings.push_back(hello_world);
    seasons_greetings.push_back(hello_world2);
    for (auto& greeting : seasons_greetings) {
        std::visit( [](auto& what){ what.Run(); }, greeting );
    }   

The bad side: You have to know all possible combinations of policies which can be really uncomfortable. But you can use some meta template stuff to create all variant types by giving type lists for each used policy and the template will generate all combinations, which is not such a big hack.

Upvotes: 1

Related Questions