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 ofAlg
instead of being a local variable inAlg::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 ofarg
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;
}
};