I like to have classes which have a valid state simply after calling their constructor - i.e. all required dependencies are passed into the constructor.
I also like required dependencies to be passed as references, because then nullptr is simply forbidden at compile-time as a value for these arguments.
Example:
class B;
class A
{
public:
A(B& b) : b(b) {}
private:
B& b;
}
After instantiating A, you are (almost) guaranteed that the instance is in a valid state. I find that code style to be very safe from programming mistakes.
My question relates to refactoring such classes when they have lots of dependencies.
Example:
// Includes for B, C, D, E, F...
class A
{
public:
A(B b, C c, D d, E e, F f) : b(b), c(c), d(d), e(e), f(f) {}
private:
B b;
C c;
D d;
E e;
F f;
}
Usually, I put long lists of parameters in structs, like this:
struct Deps
{
B b;
C c;
D d;
E e;
F f;
}
class A
{
public:
A(Deps deps) : b(deps.b), c(deps.c), d(deps.d), e(deps.e), f(deps.f) {}
private:
B b;
C c;
D d;
E e;
F f;
}
That way, it makes the call sites more explicit and less error prone as well: since all parameters must be named, you are not at risk of mistakenly switching two of them by having them in a wrong order.
Sadly, that technique works badly with references. Having references in the Deps struct forwards the problem to that struct: then, the Deps struct needs to have a constructor which initializes the references, and then that constructor will have a long parameter list, essentially solving nothing.
Now for the question: is there a way to refactor long parameter lists in constructors containing references, such that no function results in having a long parameter list, all parameters are always valid, and no instance of the class is ever in an invalid state (i.e. with some dependencies not initialized or null)?
You can't have a cake and eat it, too. Well, unless you'd use magic (also known as more powerful types).
The key idea of having the constructor take all necessary dependencies is to make sure they are all provided because the construction happens, and enforcing this statically. If you move this burden to a structure, this structure should only be passed to a constructor if all fields have been filled. If you have unwrapped references, it's obviously impossible to have this structure be only partially filled, and you can't really prove to the compiler that you'll provide the required parameters later.
You can do a run-time check, of course, but that's not what we're after. Ideally, we'd be able to encode which parameters have been initialized in the type itself. This is quite hard to implement in a generic way and only slightly easier if you make some concessions and hand-write it for specific types.
Consider a simplified example in which the types don't repeat in a signature (e.g. the signature of the constructor is ctor(int, bool, string)
). We can then use std::tuple
to represnt partially filled argument list like so:
auto start = tuple<>();
auto withIntArg = push(42, start);
auto withStringArg = push("xyz"s, withIntArg);
auto withBoolArg = push(true, withStringArg);
I've used auto
, but if you think about types of those variables, you'll realize that it will reach the desired tuple<int, string, bool>
only after all of those have been executed (albeit in random order). Then you can write the class constructor as a template accepting only tuples that indeed have all required types, write the push
function and voila!
Of course, this is a lot of boilerplate and a potential for very nasty errors, unless you take a lot of care writing the above. Any other solution you'd like to do would need to effectively do the same thing; modifying the type of the partially filled argument list until it fits the desired set.
Is it worth it? Well, you decide for yourself.
Actually, there is a pretty elegant/simple solution using std::tuple
:
#include <tuple>
struct A{};
struct B{};
struct C{};
struct D{};
struct E{};
struct F{};
class Bar
{
public:
template<class TTuple>
Bar(TTuple refs)
: a(std::get<A&>(refs))
, b(std::get<B&>(refs))
, c(std::get<C&>(refs))
, d(std::get<D&>(refs))
, e(std::get<E&>(refs))
, f(std::get<F&>(refs))
{
}
private:
A& a;
B& b;
C& c;
D& d;
E& e;
F& f;
};
void test()
{
A a; B b; C c; D d; E e; F f;
// Different ways to incrementally build the reference holder:
auto tac = std::tie(a, c); // This is a std::tuple<A&, C&>.
auto tabc = std::tuple_cat(tac, std::tie(b));
auto tabcdef = std::tuple_cat(tabc, std::tie(d, f), std::tie(e));
// We have everything, let's build the object:
Bar bar(tabcdef);
}
https://godbolt.org/z/pG1R7U
std::tie
exists precisely to create a tuple of references. We can combine reference tuples using std::tuple_cat
. And std::get<T>
allows retrieving exactly the reference we need from a given tuple.
This has:
Minimum boilerplate: You only have to write std::get<X&>
in the member initializer list for each referenced type. Nothing else needs to be provided/repeated to use this for more referenced or reference-containing types.
Complete compile-time safety: If you forget to provide a reference or provide it twice, the compiler will complain. The type system encodes all the necessary information.
No constraints on the order in which references are added.
No hand-written template machinery. Using standard facilities instead of hand-written template machinery means you don't introduce bugs/forget corner-cases. It also means users/readers of this approach have nothing they need to wade through (and might run away from, screaming).
I think this is a really simple solution, if only because std::tuple
and friends already implement all the meta-programming needed here. It's still slightly more complex than "everything in one long list", but I'm pretty sure it'll be worth the tradeoff.
(My previous hand-written template version exists in edit history. But I realized that std::tuple
does everything we need here already.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With