Reputation: 8011
This post consists of describing a problem with the straightforward implementation of the policy-based design, proposing an alternative implementation, analyzing the proposed implementation and asking help in giving correct weight to the different factors in the analysis. I apologize for the length of the post and hope that you will stick with me.
PROBLEM DESCRIPTION
Suppose that we use policy-based design as follows:
template <typename FooPolicy>
struct Alg {
void operator()() {
...
FooPolicy::foo(arguments);
...
}
};
There is a degree of coupling between the above host class and the policy: if the signature of FooPolicy::foo
changes, then the code in Alg::operator()
has to change accordingly.
The coupling becomes much tighter if policy classes are given a degree of freedom for choosing an interface. For example, suppose that FooPolicy
may implement either foo
without parameters or foo
with one integer parameter (the implementation for this case was suggested here):
template <typename FooPolicy> struct Alg {
void operator()() {
int arg = 5; // any computation can be here
// using tag dispatch to call the correct `foo`
foo(std::integral_constant<bool, FooPolicy::paramFlag>{}, arg);
}
private:
void foo(std::true_type, int param) { FooPolicy::foo(param); }
void foo(std::false_type, int param) {
(void)param;
FooPolicy::foo();
}
};
struct SimpleFoo {
static constexpr bool paramFlag = false;
static void foo();
};
struct ParamFoo {
static constexpr bool paramFlag = true;
static void foo(int param);
};
Obviously, whenever one adds a policy class with a different interface, he will have to update the dispatching mechanism, which can become complicated.
PROPOSED DESIGN
I am considering a design, whereby the algorithm would provide an interface that function members of the policy class can use to get data instead of accepting arguments. For the above example, it might look as follows:
// The policy-independent part of Alg factored out
struct AlgPolicyIndependent {
int getArg() const { return arg; }
protected:
int arg;
};
// The interface to be used by FooPolicy
struct FooPolicyServices {
FooPolicyServices(const AlgPolicyIndependent &myAlg) : alg(myAlg) {};
int getArg() const { return alg.getArg(); }
private:
const AlgPolicyIndependent &alg;
};
template <typename FooPolicy>
struct Alg : private AlgPolicyIndependent {
Alg() : fooPolicy(FooPolicyServices(*this)) {};
void operator()() {
arg = 5; // any computation can be here
fooPolicy.foo();
}
private:
FooPolicy fooPolicy;
};
struct SimpleFoo {
SimpleFoo(const FooPolicyServices &myS) : s(myS) {};
void foo() { std::cout << "In SimpleFoo" << std::endl; }
private:
const FooPolicyServices &s;
};
struct ParamFoo {
ParamFoo(const FooPolicyServices &myS) : s(myS) {};
void foo() { std::cout << "In ParamFoo " << s.getArg() << std::endl; }
private:
const FooPolicyServices &s;
};
ANALYSIS
With this design, a policy is free to use any data obtainable by using the public interface of the corresponding Services
class. In our example, ParamFoo::foo
got the algorithm's arg
by using FooPolicyServices::getArg
. The host class simply calls FooPolicy::foo
without arguments and this will not have to change even if FooPolicy::foo
changes, which is the decoupling we wanted.
I see two disadvantages to this design:
arg
has become part of the state of Alg
instead of being a local variable in Alg::operator()
, which goes against Item 26 of Effective C++ saying that variables should be defined as late as possible. However, the reasoning of that item does not apply if the cost of the extra initialization of arg
is negligible compared to the cost of running the algorithm.
Policy classes have gotten a state. So, we cannot use policies by simply calling their static member functions.
QUESTIONS
Three questions:
Is the decoupling achieved by the proposed design worth the two disadvantages listed above?
Are there disadvantages that I overlooked?
Does the proposed design have a name?
Based on the reply by @Useless, here is an updated implementation. This implementation makes it possible to have stateless policy classes, but has an overhead of passing the same reference to a Services
object each time a policy is used.
// The policy-independent part of Alg
struct AlgPolicyIndependent {
int getArg() const { return arg; }
protected:
int arg;
};
// The interface to be used by FooPolicy
struct FooPolicyServices {
FooPolicyServices(const AlgPolicyIndependent &myAlg) : alg(myAlg) {};
int getArg() const { return alg.getArg(); }
private:
const AlgPolicyIndependent &alg;
};
template <typename FooPolicy>
struct Alg : private AlgPolicyIndependent {
Alg() : fooPolicyServices(*this) {};
void operator()() {
arg = 5; // any computation can be here
FooPolicy::foo(fooPolicyServices);
}
private:
FooPolicyServices fooPolicyServices;
};
struct SimpleFoo {
static void foo(const FooPolicyServices &s) {
(void)s;
std::cout << "In SimpleFoo" << std::endl;
}
};
struct ParamFoo {
static void foo(const FooPolicyServices &s) {
std::cout << "In ParamFoo " << s.getArg() << std::endl;
}
};
Upvotes: 0
Views: 204
Reputation: 67743
There is a degree of coupling between the above host class and the policy
No. There is an equivalent degree of coupling between each of the algo and the policy, and the interface, which one requires and the other implements.
Consider the runtime-polymorphic equivalent, the Strategy:
struct IStrategy {
virtual ~IStrategy() {}
virtual void foo() = 0;
};
struct FooStrategy: public IStrategy {
void foo() override;
}
void algo(IStrategy *s) {
// ...
s->foo();
}
Now, there is exactly the same degree of coupling between the concrete strategy and the algorithm function on the (explicit base-class) interface in the Strategy pattern, as there is between the policy and algorithm template on the (implicit duck-typed) interface in the Policy pattern.
I'm not suggesting this coupling doesn't exist, but I am pointing out that this degree of coupling is not usually considered excessive.
Note there's an exact run-time analogy for your variation too - optionally implementing other (and more specific) interfaces, and using dynamic_cast
to investigate at run-time which are supported.
Is the decoupling ... worth the two disadvantages listed above?
Maybe! It depends how troublesome the coupling has become in practice, and how much state is being passed around.
In any case ...
Are there disadvantages that I overlooked?
There are plenty of disadvantages to statefulness in general, and your suggested design in particular:
this->services->value
to get an argumentWhile these are problems, they're also completely avoidable here. Just pass a local-scope context object to the policy method on every call. Then:
note that in your edit, the algorithm is still stateful. However, the services/independent things (what I called the context) can just be a local variable in the algorithm method. There's no need to preserve it outside that method, and replacing the data member with a local makes the algo stateless.
Does the proposed design have a name?
It looks like the Context pattern to me. The original suggestion looks like a sort of uncomfortable home-rolled dependency injection, but I'd stick with stateless policy & algorithm, and pass the working state around in a Context object for preference.
Upvotes: 1