Today I am pleased to announce another guest post by Matt Bentley. Matt writes about when elegance in code can hurt simplicity. If you’d like to write a guest post or co-authored post yourself, this is for you – get in touch!
Nobody’s formally defined elegance in programming, so here goes: for the purposes of this document, ‘elegance’ is defined as ‘expressing the semantic intent of code in such a way that it is both terse yet also immediately recognisable as to what the semantics are’.
An example of this in C++ is the “for” loop, which has gradually evolved from:
for (std::vector::iterator it = vec.begin(); it != vec.end(); ++it)
for (auto it = vec.begin(); it != vec.end(); ++it)
for (auto i : vec)
The problem with elegance in programming is there is an inherent tension between elegance and simplicity. Simplicity is created by having fewer things to memorize, fewer building blocks and components. The fundamental building blocks used to create the initial
for loop above are fairly straightforward – each token can be understood individually without inference because they are part of the greater structure of the language. With auto, we remove a level of obviousness, exchanging it for brevity, and thus require the programmer to infer the type whilst reading. In the third instance of the
for loop, additional understanding is now required by the programmer yet again, as it is now a domain-specific syntax which has no building blocks (aside from
for) common with the rest of the C++ language. But once the syntax is understood, it becomes easier to read.
This is not problematic in-and-of-itself – elegance should certainly be a Consideration when designing languages. But it becomes problematic when there’s an overweening focus on it, as elegance inherently leads toward domain-specific code. The most elegant solution will almost always be something which the building blocks of the language do not allow for because their expression is clumsy or verbose by comparison with a more succinct custom expression for a given scenario. But when one develops too many of these scenarios, it becomes a lot for any newcomer to take on board.
And such is the fate of modern C++. Over the past twenty years, it feels like C++ has shifted as far as is humanly possible, away from a building-block approach to coding (easy to understand), to a series of ‘elegant’ and ‘comprehensive’ approaches – where there is something specific for every case, and it is considered desirable to cater for every scenario in the most elegant fashion. The downside of this is that the wide morass of scenarios, idioms, and caveats necessary to do this are impossible to learn in their entirety. More than one eldritch C++ programmer has commented that nobody fully understands C++ anymore. Its understanding is now a hive-mind approach, resulting in a gradiated learning curve reaching an exponential point, only to be attained by the heaped corpses of a thousand burned-out programmers.
Take C by comparison – now now, I’m not going to enter a flame war, I’m just using it as an example (hush children hush!).
The building blocks are there. It is arguably as simple as it can be made without being behind the times. But it is much faster to comprehend – less expressive perhaps, less quick to write code in once understood, maybe. But the building blocks are fewer, and the edge case scenarios as introduced by needs for elegance, far fewer. Look at vector insert in C++:
iterator insert (const_iterator position, const value_type& val);
iterator insert (const_iterator position, size_type n, const value_type& val);
iterator insert (const_iterator position, InputIterator first, InputIterator last);
iterator insert (const_iterator position, value_type&& val);
iterator insert (const_iterator position, initializer_list il);
Does it really feel necessary to include five different types of insert? Yes? Why. Three would be enough – the range and initializer list ones can be replicated by the programmer with simple loops. More work for the programmer? Yes. Fewer things to memorize for the programmer and community at large? Yes. Perhaps not the strongest example, but it demonstrates a fundamental tradeoff. Every time you try and make a language more comprehensive, more elegant, more all-things-to-all-people-and-no-waste, you raise the learning bar. You additionally raise the comprehension bar for when re-reading code. Take a relatively simple language like C, however, and though there may be a lot of wasted effort in the coding process, due to the fewer amount of building blocks, the end result is code that’s simpler to write, simpler to read, and faster to learn.
Compare it to old lego vs new lego: sure, the new stuff looks smoother, but it’s all custom-builds with no imagination required. You can’t just build from scratch – you have to read the manual. At the risk of repeating myself: every time you think something can be done more expressively, more elegantly, more comprehensively – actually Think. Do we need this? Is the tradeoff between doing it with existing building blocks really worth it? Is it worth a newcomer programmer’s time and energy to learn this additional thing? If not, think back to when you were young, back to when you were first learning programming; would you want to learn the large behemoth that C++ has become? Or would you want to stay sane?
… having said all that,
std::enable_if really sucks. Roll on concepts. Just sayin’.
Do you remember, long time ago there were some people that were programming using labels and gotos? Structured programming appeared as a need to raise the level of abstraction to overcome the maintainability of such kind of programs. The language became more complex, but the code because simpler. Or, do you consider that the simplicity of label and gotos is better that the elegance of for loops?
Very recently, H. Hinnant said in his Meeting C++ 2019 keysnote that it is most important to have clear client code than clear library synopsis (or implementation), and I agree with him.
All of us have something to learn everyday.
BTW, I’m almost sure that you are using range based for loops now. If not, start to use them.
What an absolutely bizarre comment.
The point is not that abstraction isn’t worthwhile, it’s that it has to be balanced with readability. Goodbye.
A very very valid observation, C++ does suffer from becoming a behemoth without reason 🙁 .
Going a bit further than OP and thinkin aloud: to solve this problem in a practical way, we have to teach newcomers how NOT to learn those parts of C++ which are superficial (FWIW, my own favorite example of such stuff which is not worth spending time to learn, is std::copy_if() and all its numerous cousins).
And practically: is there anybody volunteering for writing a book on a “Simple (subset of) C++?” If desired by the author, I will be very willing to help with formatting / publishing / promoting such a useful book…
I would actually be reasonably keen on writing such a thing, provided there was an audience (beginners, I’m guessing). That would be taking the lower-level ‘building blocks’ approach to C++ and ignoring some of the more elegant solutions to an extent.
I have also been thinking about writing a book summarising Data-oriented Design, but I think the two topics would dovetail into each other nicely – as a simplified understanding of C++ has more in common with the DOD philosophy (which is more C-oriented) than OOP.
Won’t post my email address here, to avoid scrapers, but hit me up via twitter (xolvenz) or plflib.org 🙂