The most maintainable code uses the same lexical names and code patterns in similar contexts, to allow for seamless code refactoring. In other words, in a good codebase it's trivial to copy/paste from any function to another, to either compose or decompose a function. I encourage you to try refactoring each of the examples below into the goal, given only the starting code, and either time yourself or imagine the steps involved.
Goal
void func(Thing* pThing, Other* pOther) { if (!pThing || !pOther) { return; } pThing->doFirst(); pThing->progress += pOther->progress1; pThing->doSecond(); pThing->progress += pOther->progress2; pThing->doThird(); pThing->progress += pOther->progress3; pThing->doFourth(); pThing->progress += pOther->progress4; }
Example 1
void funcA(Thing* pThing, Other* pOther) { if (!pThing || !pOther) { return; } pThing->doFirst(); pThing->progress += pOther->progress1; funcB(*pThing, pOther); } void funcB(Thing& thing, Other* pOther) { thing.doSecond(); thing.progress += pOther->progress2; funcC(*pOther, thing); funcD(&thing, *pOther); } void funcC(Other& b, Thing& a) { a.doThird(); a.progress += b.progress3; } void funcD(Thing* thing, Other& other) { thing->doFourth(); thing->progress += other.progress4; }
Example 2
void funcA(Thing* pThing, Other* pOther) { if (!pThing || !pOther) { return; } pThing->doFirst(); pThing->progress += pOther->progress1; funcB(pThing, pOther); } void funcB(Thing* pThing, Other* pOther) { pThing->doSecond(); pThing->progress += pOther->progress2; funcC(pThing, pOther); funcD(pThing, pOther); } void funcC(Thing* pThing, Other* pOther) { pThing->doThird(); pThing->progress += pOther->progress3; } void funcD(Thing* pThing, Other* pOther) { pThing->doFourth(); pThing->progress += pOther->progress4; }
Which one was easier?
I'm gonna go out on a limb and say Example 2. It's a near-zero effort set of line-level copy&pastes. On the other hand, Example 1 requires tons of manual edits, changes in indirection, recognizing the same variable name used for both a pointer and reference (ugh, even harder to search & replace) ... the list goes on.
This is not a rant against references -- they are in fact extremely useful for perfect-forwarding, move-semantics, etc. This is a rant against mixing the use of references and pointers for the semantically same variable in different contexts.
My advice flies in the face of advice online or in the ISO C++ FAQ that heavily advocate for references wherever possible (at the expense of consistency). What I say here is -- those guys don't have to maintain your code, but YOU do. So make the smart choice!
Pointers vs References
On a related note, this code should provide a guideline for when to prefer pointers and references. Prefer pointers if any other code operating on the type also uses pointers. Otherwise, use references. Loosely speaking, this means using pointers for any heap-allocated "objects with identity", while using references for "value-type" instances defined on the stack.This is not a rant against references -- they are in fact extremely useful for perfect-forwarding, move-semantics, etc. This is a rant against mixing the use of references and pointers for the semantically same variable in different contexts.
My advice flies in the face of advice online or in the ISO C++ FAQ that heavily advocate for references wherever possible (at the expense of consistency). What I say here is -- those guys don't have to maintain your code, but YOU do. So make the smart choice!
No comments:
Post a Comment