Last active
January 7, 2021 18:56
-
-
Save d3rp/f1f86c42b45f48c3969cdca3779c9157 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
tl;dr: How to fight the formation of spaghetti code, when designing a configuration/settings | |
class, and make it feel intuitive. | |
I was toying around with my hobby project and came across a wrapper library for | |
libcurl. At first I was a little annoyed with its SSL handling and its seemingly | |
complicated way of setting it up. As I figured out how the configuration parameters | |
were supposed to be inserted in order to get things going, its ways seemed actually | |
pretty useful. Also I noticed, that apparently there's such a thing as a variadic | |
std::forward. | |
A little context before digging deeper into this. What we have here, | |
is a Settings-class, that can be configured in various ways. The concrete | |
examples shown below are toy examples, but what I was most excited about was how you could | |
instanciate and control the options on the "client side" of the code without too | |
much messyness - and still provide an intuitive way to lay out the options for | |
that Settings-object. In other words, it allowed a lot of control over the | |
option configurations while remaining reasonably simple to use and without | |
being too complicated to maintain. | |
For example, if we have a Settings-object with some context, we can set it up | |
with the use of some instanciated classes acting as options (here `Foo` and | |
`Bar`): | |
{{{ | |
Settings s(Foo(), Bar()); | |
}}} | |
This allows to configure those options quite freely by using their | |
initialization logic as we could configure `Foo()` in many ways and passing | |
the instance to the `Settings s`. | |
In order to get this going create a Settings class: | |
{{{ | |
#include <string> | |
struct Settings | |
{ | |
std::string ctx | |
} | |
}}} | |
Just to make this as simple as it needs to be in order to see its potential, | |
let's have an option object with some context that we use to manipulate the | |
Settings' context: | |
{{{ | |
#include <string> | |
struct Option | |
{ | |
std::string word = "default "; | |
} | |
}}} | |
Then adjust Settings class to use that "word" that was | |
defined above: | |
{{{ | |
struct Settings | |
{ | |
std::string ctx; | |
void set_option(Option&& option) | |
{ | |
ctx += option.word; | |
} | |
} | |
}}} | |
Lets start with the metaprogramming by creating a getter function, which | |
takes the instanciated options, creates a Settings object and uses the options | |
by passing them through the "set_options pipeline": | |
{{{ | |
template <typename... Ts> // 1 | |
Settings get(Ts&&... options) // 2 | |
{ | |
Settings s; | |
priv::set_options(s, std::forward<Ts>(options)...); // 3 | |
return s; | |
} | |
}}} | |
// 1 | |
Collecting all typenames - variadic typenames - with "typename...". | |
// 2 | |
Ts&&... - variadic move reference meaning there's one or more of "options" and | |
they can be of different types, because of the variadic typenames. | |
// 3 | |
Using a priv namespace for convenience. Passing the newly created Settings | |
object and forwarding all of the options with `std::forward<Ts>(options)...`. | |
This had a few surprising features. | |
- First, the forward allows types to be | |
infered i.e. in the end the implementation can take into account just one base | |
level interface ("Option") when the object is forwarded. | |
- Second, the syntax for this variadic pass through is | |
not all that obvious - "options" is an initializer list, for which the types are | |
collected in Ts and paired with the variadic "..." notation. | |
Next let's define the last missing piece in order to glue the | |
Settings.set_option and the getter. Here we'll use something I like to think as | |
recursive metaprogramming. I call it that, as we'll define a base case `template | |
<typename T>` and supplement that with a `template <typename T, typename... Ts>`. | |
And in this second step we snatch the first typename as a single T and the | |
remaining once with `... Ts` hence using the preprocessor for a recursion. | |
{{{ | |
namespace priv { | |
template <typename T> // 1 | |
void set_option(Settings& s, T&& t) | |
{ | |
s.set_option(std::forward<t>(t)); // 2 | |
} | |
template <typename T, typename... Ts> | |
void set_option(Settings& s, T&& t, Ts&&... ts) // 3 | |
{ | |
set_option(s, std::forward<T>(t)); | |
set_option(s, std::forward<Ts>(ts)...); // 4 | |
} | |
} | |
}}} | |
// 1 | |
Base case. When there's a call for set_option(s, t), this is used. There's | |
such a call before // 4. | |
// 2 | |
Here we use the `Settings.set_option(...)` we defined earlier. This is the | |
main piece of glue. All the other metaprogramming tricks shown here are there to | |
funnel one option at a time into this call. | |
// 3 | |
Important to collect first T alone and collect the remaining ones in Ts. | |
Again, in order to funnel a T at a time to `set_option(s, t)` | |
// 4 | |
The remaining `Ts` are looped back (recursive) into // 3 thus separating | |
the first T and collecting the others into Ts. Like with recursive functions, | |
this continues until there's nothing left for Ts in which case only // 1 is | |
called. | |
There you have it. Now to complete this, we can make other options and a main | |
function to try them out: | |
{{{ | |
struct Foo : Option // 1 | |
{ | |
Foo() : Option("foo ") {}; // 2 | |
} | |
struct Bar : Option | |
{ | |
Bar() : Option("bar ") {}; | |
} | |
void main() | |
{ | |
Settings s(Foo(), Bar()); // 3 | |
std::cout << s.ctx << "\n"; // 4 | |
} | |
}}} | |
// 1 | |
Like I mentioned earlier, `std::forward<T>(t)` and `T&& t` allows to pass | |
through infered types from `Option`. | |
// 2 | |
Just for the sake of demonstrating this, the `Option` had a field named | |
`word`, that we concatenated to `Settings.ctx`. As we've used structs, we can | |
just define the field to simulate this options-manipulating-settings-object | |
behaviour. | |
// 3 | |
This is what intrigues me: if we had more elaborate options and | |
configuration options for settings, here we could define them when instantiating | |
`Foo` and `Bar`. There's no need to mangle multiple definitions and have a | |
trickle down if-else labyrinths in some manager, but instead define the logic in | |
the options themselves, and open up the user side of the code here - as the | |
CTOR. | |
// 4 | |
Printing the wonderful results of this experiment. It should read now: "foo | |
bar" | |
This is to demonstrate, that we did in fact pass the `Foo` and `Bar` to the | |
`Settings` object, which "used them to set up itself". While passing through the | |
Options, the metaprogramming resolved the types according to the Options. | |
Now, it didn't do much in | |
this example, but I feel this can lead to cleaner code when using this pattern. | |
Such a settings class could have been configured with the use of for example | |
Enums. However, I've found that that often trickles down to an if-else | |
spaghetti part - maybe a manager or a factory - and the cleanliness of the | |
code lies on the good-will of the developer next in line to make changes to the | |
code - and not everyone feels like a boy-scout when in a hurry. In contrary to | |
that, here we use metaprogramming to help a little with the process, | |
as this enforces defining every option in their own class | |
and every set_option variation in their own overriding method. | |
My inspiration for this was cpr library for wrapping libcurl in C++ | |
https://github.com/whoshuu/cpr . More specifically, the `cpr::Get(...)` uses | |
this pattern. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment