Removing duplicate code with C++ policy-based design
Lately, I’ve been interested in some C++ template topics that help me design more flexible code. I didn’t explicitly search for them, but I met some cases that led me to find new ways of solving some specific problems.
A case is code duplication. I wanted to find a pattern for some duplicate code. I started with the idea of building components that I can compose however is needed. The first draft was using some lambdas and it was somehow OK. It did the job.
Then, looking around other aspects, I remembered that a friend keeps mentioning policy-based design. I had looked over it before, but I hadn’t found the need for it. But at that moment it clicked.
Let me get into code. I’m showing an oversimplified example because I don’t want to hide the main idea with details. If your real code is just as simple as the one I’m analyzing, do not jump on the idea before considering other approaches.
To give some hints about what follows: static polymorphism, composition, compile-time strategy. There are other approaches (runtime strategy, dynamic decoration), but I wanted to solve this at compile-time.
The goal
Straight forward, I want to remove the duplicate code using C++ policy-based design. It looks complicated at first. Just have patience.
I want to stick to the goal, so I omitted good naming, encapsulation, const correctness, noexcept and other details,
The problematic code
This is the code that has a duplicate piece of code – look for “DUPLICATE LINE” on lines 15 and 27 (for simplicity, it’s just one duplicate line):
#include <cassert> struct Object { int prop_1{}; int prop_2{}; int prop_3{}; }; void set_object_properties_1(Object& object) { if (object.prop_1 == 0) { object.prop_1 = 1; } object.prop_2 = object.prop_1 * 4; // DUPLICATE LINE object.prop_3 = object.prop_2 * 5 + object.prop_1 * 2; } void set_object_properties_2(Object& object) { if (object.prop_1 == 0) { object.prop_1 = 3; } object.prop_1 *= 2; object.prop_2 = object.prop_1 * 4; // DUPLICATE LINE object.prop_3 = object.prop_1 * object.prop_2; } int main() { Object object_1{}; set_object_properties_1(object_1); assert(object_1.prop_1 == 1); assert(object_1.prop_2 == 4); assert(object_1.prop_3 == 22); Object object_2{}; set_object_properties_2(object_2); assert(object_2.prop_1 == 6); assert(object_2.prop_2 == 24); assert(object_2.prop_3 == 144); }
An Object is used almost differently in two use cases. Only the duplicate code is common for both use cases.
A method I strongly don’t recommend
First, I want to go with a not recommended solution that I’ve seen quite a few times. Often, if they want a function to do more things, some people add another parameter to its signature. A boolean to indicate it’s a use case or another. Or another type, to support more than the two use cases that the boolean provides (true or false). This only clutters the function, gives it too many responsibilities, more testing cases, and, in the long run, difficult maintenance.
Often, such a function looks like this:
void func(int a, bool b) { if (b) { // do something with a } else { // do something else with a } }
Applied to the problematic code, the parts that are not specific to both use cases are extracted into functions like the above. And those functions are called with different arguments for the second parameter.
void init_prop_1(Object& object, int type) { switch (type) { case 1: if (object.prop_1 == 0) { object.prop_1 = 1; } break; case 2: if (object.prop_1 == 0) { object.prop_1 = 3; } object.prop_1 *= 2; break; default: // How do you handle this case? // The responsibility is yours. break; } } void init_prop_3(Object& object, int type) { switch (type) { case 1: object.prop_3 = object.prop_2 * 5 + object.prop_1 * 2; break; case 2: object.prop_3 = object.prop_1 * object.prop_2; break; default: // How do you handle this case? // The responsibility is yours. break; } } void set_object_properties_by_type(Object& object, int type) { init_prop_1(object, type); object.prop_2 = object.prop_1 * 4; init_prop_3(object, type); } int main() { Object object_1b{}; set_object_properties_by_type(object_1b, 1); Object object_2b{}; set_object_properties_by_type(object_2b, 2); }
I’ve introduced too many branches with this solution, too many extra aspects to take care of. For each extra parameter in a function, you have to spend more time understanding what it does. Shortly, if you think this method is good, we cannot be friends.
The first approach with C++ policy-based design
It starts with a class (struct for simplicity) the contains the duplicate code and it is given two other classes that each will implement the code for each use case. If you are thinking “I didn’t even see code and he is already talking about three classes”, I want to remind you that the example code is very simple to point out the idea.
The two other classes are given as template parameters because the entire solution is solved at compile-time. The classes each have a method that contains the use case specific code. And they are called policies. In one situation, the implementation works with some policies, in the other situation with some other policies.
template <typename prop_1_policy, typename prop_3_policy> struct set_object_properties { static void set(Object& object) { prop_1_policy::init(object); object.prop_2 = object.prop_1 * 4; prop_3_policy::init(object); } };
The policies for settings prop_1 in both use cases:
struct init_prop_1a { static void init(Object& object) { if (object.prop_1 == 0) { object.prop_1 = 1; } } }; struct init_prop_1b { static void init(Object& object) { if (object.prop_1 == 0) { object.prop_1 = 3; } object.prop_1 *= 2; } };
And the ones for prop_3:
struct init_prop_3a { static void init(Object& object) { object.prop_3 = object.prop_2 * 5 + object.prop_1 * 2; } }; struct init_prop_3b { static void init(Object& object) { object.prop_3 = object.prop_1 * object.prop_2; } };
You can see that each policy is very specific and simple. They each have a function that receives an object to apply a specific operation to it.
Please disregard the fact that the functions could have returned a value instead of working on a reference to the object. That’s another topic that you can handle with the best approach for your situation.
Then, the implementation for each use case can be created by composition as follows:
using set_object_1_properties = set_object_properties<init_prop_1a, init_prop_3a>; using set_object_2_properties = set_object_properties<init_prop_1b, init_prop_3b>;
And the usage is:
Object object_1c{}; set_object_1_properties::set(object_1c); Object object_2c{}; set_object_2_properties::set(object_2c);
The greatness that comes with this approach is that each configured use case (set_object_1_properties, set_object_2_properties) is very specific. There are no additional decisional branches. Each implementation does its job and end of the story.
Performance-wise, no runtime overhead is added. Everything is resolved at compile-time and will be inlined (depending on the complexity of your business code and how you implement the policies, there can be cases where not everything is inlined).
For many applications, some extra if statements do not affect performance, but you can get into situations where they do. For my showcase, nothing is added, the hot path is clear as it’s only one path.
More flexibility
To go even further, there is one more aspect that you will find in real life: to configure some values from outside the classes. All those numbers that I’ve thrown inside the business logic could be passed from outside of the policies. This makes the code even more flexible because it can be used in more use cases.
As an example, I will extract the initial value given to the property if it’s zero. For this, I need a configuration struct that is given to the policies.
struct init_config { int init{}; }; struct init_prop_1a_configurable { init_config& config_; void init(Object& object) { if (object.prop_1 == 0) { object.prop_1 = config_.init; } } }; struct init_prop_1b_configurable { init_config& config_; void init(Object& object) { if (object.prop_1 == 0) { object.prop_1 = config_.init; } object.prop_1 *= 2; } };
The policies for prop_3 remain the same because they are not interested in the configuration.
struct init_prop_3a_configurable { static void init(Object& object) { object.prop_3 = object.prop_2 * 5 + object.prop_1 * 2; } }; struct init_prop_3b_configurable { static void init(Object& object) { object.prop_3 = object.prop_1 * object.prop_2; } };
And the class that is given the policies will pass that configuration to each interested policy.
template <typename prop_1_policy, typename prop_3_policy> struct set_object_properties_configurable : prop_1_policy, prop_3_policy { set_object_properties_configurable(init_config& config) : prop_1_policy{config}, prop_3_policy{} {} void set(Object& object) { prop_1_policy::init(object); object.prop_2 = object.prop_1 * 4; prop_3_policy::init(object); } };
You compose the use cases:
using set_object_1_properties_configurable = set_object_properties_configurable<init_prop_1a_configurable, init_prop_3a_configurable>; using set_object_2_properties_configurable = set_object_properties_configurable<init_prop_1b_configurable, init_prop_3b_configurable>;
And then you create and pass the configuration:
// Object 1 Object object_1d{}; init_config object_1_config{}; object_1_config.init = 1; set_object_1_properties_configurable object_1_setter{object_1_config}; object_1_setter.set(object_1d); // Object 2 Object object_2d{}; init_config object_2_config{}; object_2_config.init = 3; set_object_2_properties_configurable object_2_setter{object_2_config}; object_2_setter.set(object_2d);
Composition gives you full power because you can combine the policies as you wish, providing support for multiple use cases. You can take the prop_1 policy for the second object and the prop_3 policy for the first object:
using set_object_3_properties_configurable = set_object_properties_configurable<init_prop_1b_configurable, init_prop_3a_configurable>;
And if you need even more flexibility, the next step would be to also extract the common logic into a policy.
Choose what you actually need
I have mentioned the good and bad parts of the methods in each of their section above. But don’t take anything for granted.
For me, there are many good sides to this approach. To name a few: performance, clarity, simplicity, specificity, fewer branches, independent tests, composable objects, the Single-responsibility principle.
Always consider your actual need before jumping to conclusions.
#include <cassert> struct Object { int prop_1{}; int prop_2{}; int prop_3{}; }; // Duplicate void set_object_properties_1(Object& object) { if (object.prop_1 == 0) { object.prop_1 = 1; } object.prop_2 = object.prop_1 * 4; // duplicate object.prop_3 = object.prop_2 * 5 + object.prop_1 * 2; } void set_object_properties_2(Object& object) { if (object.prop_1 == 0) { object.prop_1 = 3; } object.prop_1 *= 2; object.prop_2 = object.prop_1 * 4; // duplicate object.prop_3 = object.prop_1 * object.prop_2; } // Not recommended void init_prop_1(Object& object, int type) { switch (type) { case 1: if (object.prop_1 == 0) { object.prop_1 = 1; } break; case 2: if (object.prop_1 == 0) { object.prop_1 = 3; } object.prop_1 *= 2; break; default: // How do you handle this case? // The responsibility is yours. break; } } void init_prop_3(Object& object, int type) { switch (type) { case 1: object.prop_3 = object.prop_2 * 5 + object.prop_1 * 2; break; case 2: object.prop_3 = object.prop_1 * object.prop_2; break; default: // How do you handle this case? // The responsibility is yours. break; } } void set_object_properties_by_type(Object& object, int type) { init_prop_1(object, type); object.prop_2 = object.prop_1 * 4; init_prop_3(object, type); } // Better struct init_prop_1a { static void init(Object& object) { if (object.prop_1 == 0) { object.prop_1 = 1; } } }; struct init_prop_1b { static void init(Object& object) { if (object.prop_1 == 0) { object.prop_1 = 3; } object.prop_1 *= 2; } }; struct init_prop_3a { static void init(Object& object) { object.prop_3 = object.prop_2 * 5 + object.prop_1 * 2; } }; struct init_prop_3b { static void init(Object& object) { object.prop_3 = object.prop_1 * object.prop_2; } }; template <typename prop_1_policy, typename prop_3_policy> struct set_object_properties { static void set(Object& object) { prop_1_policy::init(object); object.prop_2 = object.prop_1 * 4; prop_3_policy::init(object); } }; using set_object_1_properties = set_object_properties<init_prop_1a, init_prop_3a>; using set_object_2_properties = set_object_properties<init_prop_1b, init_prop_3b>; // Even better - configurable struct init_config { int init{}; }; struct init_prop_1a_configurable { init_config& config_; void init(Object& object) { if (object.prop_1 == 0) { object.prop_1 = config_.init; } } }; struct init_prop_1b_configurable { init_config& config_; void init(Object& object) { if (object.prop_1 == 0) { object.prop_1 = config_.init; } object.prop_1 *= 2; } }; struct init_prop_3a_configurable { static void init(Object& object) { object.prop_3 = object.prop_2 * 5 + object.prop_1 * 2; } }; struct init_prop_3b_configurable { static void init(Object& object) { object.prop_3 = object.prop_1 * object.prop_2; } }; template <typename prop_1_policy, typename prop_3_policy> struct set_object_properties_configurable : prop_1_policy, prop_3_policy { set_object_properties_configurable(init_config& config) : prop_1_policy{config}, prop_3_policy{} {} void set(Object& object) { prop_1_policy::init(object); object.prop_2 = object.prop_1 * 4; prop_3_policy::init(object); } }; using set_object_1_properties_configurable = set_object_properties_configurable<init_prop_1a_configurable, init_prop_3a_configurable>; using set_object_2_properties_configurable = set_object_properties_configurable<init_prop_1b_configurable, init_prop_3b_configurable>; // Compose using set_object_3_properties_configurable = set_object_properties_configurable<init_prop_1b_configurable, init_prop_3a_configurable>; int main() { // Duplicate Object object_1{}; set_object_properties_1(object_1); assert(object_1.prop_1 == 1); assert(object_1.prop_2 == 4); assert(object_1.prop_3 == 22); Object object_2{}; set_object_properties_2(object_2); assert(object_2.prop_1 == 6); assert(object_2.prop_2 == 24); assert(object_2.prop_3 == 144); // Not recommended Object object_1b{}; set_object_properties_by_type(object_1b, 1); assert(object_1b.prop_1 == 1); assert(object_1b.prop_2 == 4); assert(object_1b.prop_3 == 22); Object object_2b{}; set_object_properties_by_type(object_2b, 2); assert(object_2b.prop_1 == 6); assert(object_2b.prop_2 == 24); assert(object_2b.prop_3 == 144); // Better Object object_1c{}; set_object_1_properties::set(object_1c); assert(object_1c.prop_1 == 1); assert(object_1c.prop_2 == 4); assert(object_1c.prop_3 == 22); Object object_2c{}; set_object_2_properties::set(object_2c); assert(object_2c.prop_1 == 6); assert(object_2c.prop_2 == 24); assert(object_2c.prop_3 == 144); // Even better - configurable Object object_1d{}; init_config object_1_config{}; object_1_config.init = 1; set_object_1_properties_configurable object_1_setter{object_1_config}; object_1_setter.set(object_1d); assert(object_1d.prop_1 == 1); assert(object_1d.prop_2 == 4); assert(object_1d.prop_3 == 22); Object object_2d{}; init_config object_2_config{}; object_2_config.init = 3; set_object_2_properties_configurable object_2_setter{object_2_config}; object_2_setter.set(object_2d); assert(object_2d.prop_1 == 6); assert(object_2d.prop_2 == 24); assert(object_2d.prop_3 == 144); // Compose Object object_3d{}; init_config object_3_config{}; object_3_config.init = 6; set_object_3_properties_configurable object_3_setter{object_3_config}; object_3_setter.set(object_3d); assert(object_3d.prop_1 == 12); assert(object_3d.prop_2 == 48); assert(object_3d.prop_3 == 264); }