Hacker News new | ask | show | jobs
by badsectoracula 2228 days ago
I've worked on a bunch of engines, both big AAA and small indie scaled ones and i agree with you. It is actually from this experience that i have a hard dislike for C++'s "auto" outside of cases where you can't do otherwise - it makes the code you didn't write hard to understand exactly what is going on (and sometimes error prone). Sure IDEs can show you the type if you mouse over (at least some of them), but if the type is explicitly typed you do not need to do that and you can just read instead of pause, move the mouse over the auto, read the type, then move the mouse to the next (if any), etc. And that is assuming you are reading the code inside an IDE - it doesn't work if you are reading the code in a web-based code review tool that at best can show you syntax highlighting (and it is exactly in that environment where you want the code to be at its most understandable).

Now not all features are bad, lambdas are OK when used as local functions and can make the code more readable if the alternative is to define some static function outside the current method (e.g. you want to pass some custom filter or comparator). They can certainly be abused though, but it is one of those cases where their usefulness is greater than their abuses (and i can't say the same for "auto").

For the example given... it might be a bad example, but honestly i was looking at that code for a bit and i simply cannot read it - i do not understand what is going on just by reading the code, i'd need to run it in a debugger and go through it step by step (and i've actually written texture atlas packers before). It completely fails to sell me on the "fold expressions" and "parameter packs" and it certainly doesn't look at all "elegant" to me (but note that it might be that the example is awful, not the language feature itself).

And it did make me skim through the rest of the article though since after completely failing me on all fronts at the introduction bits, i couldn't get the feel that i have any common grounds with the author.

3 comments

You're exaggerating. There's nothing complicated in the code - it looks straightforward to me, and I've never written an "atlas packer". The only slightly confusing line is the one before the last: I believe you should not use side-effects when unpacking the fold expression; if you need an external state, use a proper loop construct (EDIT: although, to be fair, it might be impossible in this case, unless you can reify the fold into runtime, or get a special-purpose loop). But everything else is straightforward, and I could understand the algorithm just fine. The higher-level constructs, which people seem to hate in this thread, make the code shorter and more general, and also very familiar to people using (properly) higher-level languages - for example, the code here is very similar to how you'd write a macro using syntax-rules in Scheme. If it performs the same or better than other, more explicit and verbose, ways of writing the same algorithm, then it's a win overall, and the approach should not be dismissed just because you're not familiar with the features used. Well, I managed to read this snippet just fine while I don't know modern C++ at all (last worked with C++ when Y2K was still a thing), so a professional C++ developer should be able to grok this effortlessly.
Eh, no, i'm not exaggerating. I really have a hard time following the flow of the posted code. I can get a rough idea of what it is doing by ignoring most of the Modern C++-isms, but i still can't tell you with confidence that i know exactly what is going to happen (...and i'm not asking for an explanation, btw, that is besides the point :-P).

I mean, sure, if i take that code and run it through a debugger - perhaps while also crossreferencing the features it uses at cppreference.com - then i'd be able to follow it. However at that point any relevance to readability would have been thrown out of the window long ago.

Well, readability is like this. It's not a property of the code alone, it's an emergent property based on both the code and your knowledge as a reader. It's very, very subjective, which our little disagreement here proves. Basically, the same code using the same feature can be both insurmountable wall of text and an elegant, readable solution - depending on your background, current knowledge, and personal taste (among other factors).

For me, this whole spread/fold feature is easy to grok, because I've worked with many similar features elsewhere. In this case, the feature looks almost identical to how `...` is handled in one of the Scheme macro systems, syntax-case (and syntax-rules, by extension)[1]. As mentioned, the use of spread on a comma operator with a side-effect is tricky and maybe too clever, but, otherwise, I don't see anything out of ordinary.

> perhaps while also crossreferencing the features it uses at cppreference.com

That's the thing - if you already knew the meaning and syntax of these features by heart, you'd find the code using them very readable. You also wouldn't need to step through it in the debugger, because there's really not much happening there in terms of control flow.

In general, "readability" is simply a bad word to use: it's too overloaded and means too many things to too many (kinds of) people. Every language can become readable to you if you put enough effort into it; and no, the amount of effort needed is also dependent more on your prior knowledge than on the language in question. So it's just too subjective to be useful as a metric for anything, unfortunately.

[1] https://docs.racket-lang.org/reference/stx-patterns.html#%28...

It isn't just about knowledge, but also about how much knowledge you'd need to keep in your head just to read something - the least the code requires from you before you even start reading the code, the more you can focus on understanding the code itself. And even when you know about the features shown, it still is hard to follow the flow. I mean, i do know about lambdas in C++ and have used them a lot, but i can still find it harder to follow code that uses them extensively with the flow jumping around as, e.g., calls to other functions call back to local lambdas.
> but also about how much knowledge you'd need to keep in your head just to read something

Yeah, but the effect of this is greatly overstated most of the time. As I said, given enough effort, you can learn - and learn to keep in your head - anything. It matters in the short term, while you're learning, but in the long run, once you've learned and internalized all the required information, it stops being relevant.

It's probably harder to learn to read and write kanji instead of the Latin alphabet. For most people, that difference matters for a few years in their childhood, but once they have the characters drilled into them, it no longer matters: they can read and write as well as any Westerner.

The same is true for (natural) languages: some are inherently more complex and hard to learn than others, yet once you become fluent, you stop noticing the complexity. You simply speak, read, and write your thoughts directly, without thinking about grammar and spelling too much.

It's also visible in sciences and engineering. Mathematical notation is especially notorious: not only every symbol can have multiple meanings, but you're expected to also guess which meaning was intended from other symbols and text around. That's on top of introducing hundreds of made-up words for equally made-up concepts, like a "number", or "monoid in the category of endofunctors".

Finally, it manifests in programming and programming languages. In various ways. For example, there are some people who use APL, K, or J - because it's "easier to read and keep in your head a single line of APL than a 500 loc of equivalent C". If given a chance, they will tell you that something like this is of course very readable and straightforward:

    ⍝ John Conway's "Game of Life".
    life←{↑1 ⍵∨.∧3 4=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂⍵}
You just need to learn a few things first, and that may be hard, but once you do - I'm told - reading and writing code this way becomes effortless, and a thousand times more efficient than writing in C.

Basically, if you're going to be switching languages every year, then yes, there's a difference between having to learn the language for a month rather than six before you can ship something. On the other hand, if you're going to stick with a language for a decade or two, then the long learning process becomes irrelevant, as it's dwarfed by the rest of the time where you actually use the language.

> it still is hard to follow the flow

It may be hard if you're not familiar with the common patterns of using higher-order functions. HOF and lambdas are not GOTO: there's a structure there, it's just richer than the basic set of if/for/while statements. You could call such a structure an FP equivalent of OOP design patterns.

> calls to other functions call back to local lambdas

Yeah, but that's also true for every abstraction, starting with a procedure definition. Also, you don't need lambdas to have this problem, it's enough to register procedure as a signal handler, or register an event handler in some async framework. When you pass a comparator function to `qsort`, you similarly don't know when and how that function will be called, even though it's a named procedure.

To summarize: no matter the language, you can learn it, you can fit all of it in your head, and you can make it readable for you. It requires effort, which is an investment: it might not be worth your while, depending on your circumstances. However, if you encounter a code you don't understand or have trouble with reading, because you didn't invest enough time into learning the language, that's not the code's (or features') fault. Just be honest with yourself and don't blame others for what is a result of your conscious decision.

Also of note: yes, the features often do differ in their complexity, and the differences influence the readability (for lack of a better word). However, to see this and to be able to compare, you have to first learn the features in-depth.

Because there is yet no proper for loop for argument packs or tuple-like objects, fold expressions over the comma operators are unfortunately the next best thing.

Edit: also the default comma operator discards its lhs, it is pretty much always used for its side effects.

Yeah, I figured it might not be currently possible. I was thinking about something like Scala HList[1], which provides a map/flatMap (which enables for-loop) method for tuples (among other functionality).

[1] https://github.com/milessabin/shapeless/wiki/Feature-overvie...

boost.fusion, boost.hana provide similar functionality (i.e. arbitrary runtime or compile time transformations over tuple-like objects) but they are relatively large dependencies and it is not worth it just for a tuple for-each.
I wonder how wide spread dislike of "auto" is in c++ (I've seen that a few places) when the equivalent type inference has become fairly standard and preferred in other languages like C#, Rust, Go, etc...
With Java I've seen coding standards that you can use `var` only when type is obvious from declaration. For example:

    var person = new Person();
    var car = selectCarById(carId); // Car
if type is not obvious, it should be explicitly declared.
I'd avoid the second example since you'd need to know the return type of selectCarById to know what the actual type that will be returned is (the name doesn't help, it might return something like, say, a "ref<Car>" or something like that - e.g. in a game engine i worked on a couple of years ago all resource pointers were passed around encapsulated in a special template that handled automatic resource management - methods would still be called something like "GetMesh" but what you'd get wouldn't be a "Mesh" but a "TResRef<Mesh>", however since in other places in the engine you'd work with "raw" Mesh types, unless you knew what GetMesh returned - which could be the case for, e.g., some programmer that normally worked with at a completely different subsystem with its own rules - you'd might expect a "auto mesh = foo->GetMesh()" to be a "Mesh" but instead it is "TResRef<Mesh>").
This is also common in the C++ community. Clang-tidy has an auto fix for this that can be applied to code bases.
I sometimes see people stating exactly this, but then writing:

`doSomethingToACar(selectCarById(carId));`

Kind of weakens the argument. I'm not sure what's the best approach, but I'm usually ok with autos even when the type is not explicitly known - when reading code, I do not really need to know what exact type a variable has ("it's a car, goddamnit, it says so in the name!"), just how it's used (and then meaningful function names become very important).

Traditionally C++ code is often considered harder to read than code in these other languages, and the "excessive" use of 'auto' does not make understanding code easier. Still, according to my observations the split in opinions on this is about 50/50; mine is that the use of 'auto' improves the "genericity" of code (on par with the use of templates) and its amenability to refactoring with less chance to make a mistake. As to the readability of code, it also improves due to not having to repeat yourself as often - as long as the names of the variables remain self-describing or are clear from the context.
I'd also say that using auto makes your code easier to read, especially when you are the user of generic code. Looping through a vector where you need to keep track of the iterator, for example.

    for(auto iter = vec.begin(); iter!=vec.end(); iter++)
    for(std::vector<project_namespace::class_name>::iterator iter = vec.begin(); iter!=vec.end(); iter++)
These days there is also:

    for (auto i : vec)
Yeah, this is exactly what i dislike - unless the declaration of "vec" is somewhere close by (and assuming it isn't itself "auto" :-P) you have no idea what "i" is.

Especially when that "auto i : vec" should have instead been "auto& i : vec" or "const auto& i : vec" and now you are at best wasting cycles and at worst writing to copies that will soon be discarded, ending up with a bug that can be very hard to spot.

I love the idea of auto range-based iteration but it's full of warts like the one you mention. Recently I found myself wanting an iterator over a combinatorial family.

Generation of a single solution: 3 easy lines (calling on a few hundred lines of goofy math that actually describes the structure, but that's common to all of these approaches)

Writing a for-loop to fill a std::vector of solutions -- about 10 lines of a familiar stack-walking pattern which could confuse a novice.

Making a fake container that defines a begin() and end() along with a nested iterator class: about 20 lines of necessary boilerplate, another 20 lines to replicate the stack-walking, now sprinkled about the boilerplate. The novice is completely bewildered, so we add another 10-20 lines of comments to explain it.

So I have this strong urge to keep the first two implementations in place, just to provide a gentler ramp. But I won't use the code in the end, so it would only add maintenance overhead, so a lone tear rolls down my cheek as I delete the clear, readable code.

In python, this is often as easy as changing square brackets to parentheses to change a list comprehension into a generator.

I usually write auto &i : vec out of reflex, but left the reference part out into visually match what the parent had, with no reference. (But an iterator, so not having that issue.)
If the type is not obvious one can also write

    for(Class entry : container)
This is still an uncontroversial improvement over having to typedef or use auto for the iterator.
I think pretty much every one agree with auto for iterators and duplicated types (casts, initialisation).

The debate is about all the other cases.

There is definitely disagreement in C# as to the proper usage of var.
Perhaps the people who dislike auto in C++ would also dislike the equivalent feature in other languages but they just happen to not work in them?

I know i do not use any of the languages you mention, for example - and if i did, i'd explicitly write any type names.

> Perhaps the people who dislike auto in C++ would also dislike the equivalent feature in other languages but they just happen to not work in them?

How do you reconcile that world view with the fact that people are shipping billions of line of codes that obviously work in languages where until recently you couldn't even write any type anywhere (JS, Python) ?

I'm not sure what is there to reconcile or even what world view you refer to. Personally i do not use these languages much and when i do it is usually very short code and looks very different to code i'd write in a language with static strong typing.
I don't think so. auto adds some more complications in C++ than var or let in other languages. Consider "const auto& a = x" vs "auto a = x". What exactly is the type of a? It depends.
Auto makes it harder to know the type in question, if C++'s auto is slightly more cryptic than var or let in other languages, doesn't really matter that much if what you dislike is not knowing the type in question in the first place.

But honestly i can only talk about me here, i can't guess why some imaginary other developer who dislikes a feature does dislike it.

I think it was welcomed with open arms by less experienced devs because it made code easier to compile, and rightly so. C++'s compiler messages are off-putting.

For others, it was worrisome because it made code easier to compile, and rightly so. If it complies it doesn't mean it's correct.

An all or nothing approach to auto ends up being silly for reasons of clarity and specificity in types. Auto with compound types makes programs easier to read and write, especially if an IDE is there to expand complex type information. If the type is small, basic, intrinsic, etc. then auto can be a hindrance.
> An all or nothing approach to auto ends up being silly

I already wrote that there are some cases where auto is necessary (usually when used with more recent C++ features).

> especially if an IDE is there to expand complex type information

And i also already wrote that this information is not only often cumbersome to obtain but also such an IDE is often not available - e.g. in a web-based code review tool which also happens to be an environment where you want the code to be most understandable.

What I'm saying is that auto is very useful and not an exotic or niche feature, it just works the best when not using it in places where a type definition is already small or direct.

This usually means types that are from inside the scope of another class. Compound types that are used frequently can actually be aliased.

Also writing programs that are clear when reading from plain text is great, but I don't think that should ever be a higher priority then what it is like to work with inside an IDE. The days of writing programs with notepad are over thankfully. Languages aren't the only way to make programming easier and aren't even where the low hanging fruit is. People get caught up in languages, but tools can help much more without the herculean effort of redoing decades of work, so I lean on them whenever possible.

Well, i already wrote about my thoughts on auto, so i do not see a reason to repeat them.

However, about IDEs, you still ignore that code is not only worked with inside IDEs - i already wrote twice the case of a code review tool... have you ever worked on a team with code reviews done by a web-based tool? Or even with a source control program that you want to check the differences between commits that someone else made long ago (they may not even be at the company anymore) and the diff tool obviously has no idea about types and such?

There are many reasons for why you need to work with code outside of an IDE and none of them have to do with using Notepad to write the code.