It's a bit esoteric to say the least as you already recognized which might have me scratching my head for a moment when I first start encountering your code wondering what you're doing and where these helper classes are implemented until I start to pick up your style/habits (at which point I might get totally used to it).
I do like that you're reducing the amount of information in the headers. Especially in very large codebases, that can have practical effects to reduce compile-time dependencies and ultimately build times.
My gut reaction though is that if you feel a need to hide implementation details this way, to favor parameter passing to free-standing functions with internal linkage in the source file. Usually you can implement utility functions (or whole classes) useful for implementing a particular class without having access to all the internals of the class and instead just pass in the relevant ones from the implementation of a method to the function (or constructor). And naturally that has the bonus of reducing coupling between your class and the "helpers". It also has a tendency to generalize what might have otherwise been "helpers" further if you find that they're starting to serve a more generalized purpose applicable to more than one class implementation.
I also sometimes cringe a bit when I see lots of "helpers" in code. It's not always true but sometimes they can be symptomatic of a developer who is just decomposing functions willy-nilly to eliminate code duplication with huge blobs of data being passed around to functions with barely comprehensible names/purposes beyond the fact that they reduce the amount of code required to implement some other functions. Just a little teeny bit more thought upfront can sometimes lead to much greater clarity in terms of how the implementation of a class is decomposed into further functions, and favoring passing specific parameters to passing whole instances of your object around with full access to internals can help promote that style of design thought. I'm not suggesting you're doing that, of course (I have no idea), but maybe to watch out for that tendency when we get too happy with the "helpers".
If that becomes unwieldy, I'd consider a second, more idiomatic solution which is the pimpl (I realize you mentioned issues with it but I think you can generalize a solution to avoid those with minimal effort). That can move a whole lot of information your class needs to be implemented including its private data away from the header wholesale. The performance issues of the pimpl can largely be mitigated with a dirt-cheap constant-time allocator* like a free list while preserving value semantics without having to implement full-blown user-defined copy ctor.
- For the performance aspect the pimpl does introduce a pointer overhead at the very least, but I think the cases have to be pretty serious where the poses a practical concern. If the spatial locality isn't degraded significantly through the allocator, then your tight loops iterating over the object (which should generally be homogeneous if performance is that much of a concern) will still tend to minimize cache misses in practice provided you use something like a free list to allocate the pimpl, putting the fields of the class into largely contiguous memory blocks.
Personally only after exhausting those possibilities would I consider something like this. I do think it's a decent idea if the alternative is like more private methods exposed to the header with perhaps only the esoteric nature of it being the practical concern.
One alternative that popped into my head just now that largely accomplishes your same purposes absent friends is like this:
bool match() const;
// In source file:
static bool fullMatch(const PredicateListData& p)
// Can access p.somePrivateField here.
bool PredicateList::match() const
Now that might seem like a very moot difference and I'd still call it a "helper" (in a possibly derogatory sense since we're still passing the entire internal state of the class to the function whether it needs it all or not) except it does avoid the "shock" factor of encountering
friend. In general
friend looks a bit scary to see frequently absent further inspection, since it says that your class internals are accessible elsewhere (which carries the implication that it might be incapable of maintaining its own invariants). With the way you are using
friend it becomes rather moot if people are aware of the practice since the
friend is just residing in the same source file helping to implement the private functionality of the class, but the above accomplishes much the same effect at least with the one possibly arguable benefit that it doesn't involve any friends which avoids that whole kind ("Oh shoot, this class has a friend. Where else does its privates get accessed/mutated?"). Whereas the immediately above version immediately communicates that there's no way for the privates to be accessed/mutated outside of anything done in the implementation of
That is perhaps moving towards somewhat dogmatic territories with this level of nuance since anyone can quickly figure out if you uniformly name things
*Helper* and put them all in the same source file that it's all kind of bundled together as part of the private implementation of a class. But if we get nit-picky then maybe the immediately above style won't cause as much of a knee-jerk reaction at a glance absent the
friend keyword which tends to look a little bit scary.
For the other questions:
A consumer could define their own class PredicateList_HelperFunctions, and let them access the private fields. While I don't see this as a huge issue (if you really wanted at those private fields you could do some casting), maybe it would encourage consumers to use it that way?
That might be a possibility across API boundaries where the client could define a second class with the same name and gain access to the internals that way without linkage errors. Then again I'm largely a C coder working in graphics where safety concerns at this level of "what if" are very low on the priority list, so concerns like these are just ones I tend to wave my hands at and do a dance and try to pretend like they don't exist. :-D If you're working in a domain where concerns like these are rather serious though, I think that's a decent consideration to make.
The above alternative proposal also avoids suffering this issue. If you still want to stick to using
friend though, you can also avoid that issue by making the helper a private nested class.
// Declare nested class.
// Make it a friend.
friend class Helper;
// In source file:
Is this a well-known design pattern that there's a name for?
None to my knowledge. I kind of doubt there would be one since it's really getting into the minutia of implementation details and style.
I got a request for further clarification on the point about how I sometimes cringe when I see implementations with lots of "helper" code, and that might be slightly controversial with some but it is actually factual as I really did cringe when I was debugging some of my colleagues' implementation of a class only to find loads of "helpers". :-D And I wasn't the only on the team scratching my head trying to figure out what all these helpers are supposed to do exactly. I also don't want to come off dogmatic like "Thou shalt not use helpers," but I would make a tiny suggestion that it might help to think about how to implement things absent of them when practical.
Aren't all private member functions helper functions by definition?
And yes, I am including private methods. If I see a class with like a straightforward public interface but like an endless set of private methods which are somewhat ill-defined in purpose like
find_helper, then I also cringe in a similar way.
What I'm suggesting as an alternative is nonmember nonfriend functions with internal linkage (declared
static or inside an anonymous namespace) to help implement your class with at least a more generalized purpose than "a function which helps implement others". And I can cite Herb Sutter from C++ 'Coding Standards' here for why that can be preferable from a general SE standpoint:
Avoid membership fees: Where possible, prefer making functions nonmember nonfriends. [...] Nonmember nonfriend functions improve encapsulation by minimizing dependencies: The body of the function cannot come to depend on the nonpublic members of the class (see Item 11). They also break apart monolithic classes to liberate separable functionality, further reducing coupling (see Item 33).
You can also understand the "membership fees" he talks about to some degree in terms of the basic principle of narrowing variable scope. If you imagine, as the most extreme example, a God object which has all the code required for your entire program to run, then favoring "helpers" of this sort (functions, whether member functions or friends) which can access all the internals (privates) of a class basically render those variables no less problematic than global variables. You have all the difficulties of managing state correctly and thread safety and maintaining invariants that you would get with global variables in this most extreme example. And of course most real examples are hopefully not anywhere close to this extreme, but information hiding is only as useful as it is limiting the scope of the information being accessed.
Now Sutter already gives a nice explanation here but I'd also add further that the decoupling tends to promote like a psychological improvement (at least if your brain works like mine) in terms of how you design functions. When you start designing functions that can't access everything in the class except only the relevant parameters you pass it or, if you pass the instance of the class as a parameter, only its public members, it tends to promote a design mindset that favors functions which have a clearer purpose, on top of the decoupling and promoting improved encapsulation, than what you might otherwise be tempted to design if you could just access everything.
If we go back to the extremities then a codebase riddled with global variables doesn't exactly tempt developers to design functions in a way that's clear and generalized in purpose. Very quickly the more information you can access in a function, the more many of us mortals face the temptation to degeneralize it and reduce its clarity in favor of accessing all this extra information that we have instead of accepting more specific and relevant parameters to that function to narrow its access to state and widen its applicability and improve its clarity of intentions. That applies (although generally to some lesser degree) with member functions or friends.