"A junior engineer writes comments that explain what the code does. A mid-level engineer writes comments that explain why the code does what it does. A senior engineer writes comments that explain why the code isn't written in another way."
(except punchier, of course. I'm not doing the quip justice here)
Exactly! I have written code that required comments five times as long as the code itself to defend the code against well meaning refactoring by people who did not understand the code, the domain, or the care needed to handle large dynamic ranges.
I have also written substantial functions with no comments at all because it was possible and practical to name the function and all the variables so that the meaning was clear.
It's fun and enlightening to go ham on any particular style/framework/philosophy, but actually living by dogma in prod gets kinda dangerous and imo is counter to the role of a senior+ engineer
I have regretted following that lesson ever since.
So, less comments wins out, but faced with a code base without comments you have to inspect it to tell the difference between a beautifully crafted piece of brilliance or a totally flaky codebase written by a bunch of newbie hacks.
This is a common experience when I use a new language or framework. At the start I comment a lot of things because I think it's useful for future me, but as I learn more I realize that many of the comments are redundant so I end up removing them.
If it's not answering why or at least providing a very concise how to very verbose code, then it's just adding noise
And C-level engineers write a comment "X hours have been wasted on refactoring this code. Should you decide following the example of your priors, please increment this counter."
Sometimes, even what appears to be an utter hack job actually is the best you're gonna get.
I'm apologising because the code isn't obvious, or the language not sufficiently expressive, or the good-idea-at-the-time no longer is. Ideally I wouldn't need to write many comments, but I often find myself sorry things are not simpler.
You are right in spirit to say sorry when the code cannot be self-explainatory, but consider that sometimes even if the code is self explainatory, comments can help your reader to see the grand picture faster or to avoid interpersonal ambiguity by allowing you to use two different ways of phrasing a thing.
Cursor cursor = getCursor(); // Cursor. class User {
public String getName() { ... }
}
to have a doc comment explaining what getName does and also what it returns. Before git, they sometimes needed to see who wrote it and when.Somethings I include brief explanations or links to a particular piece of hard-to-find documentation.
I suggest providing that sort of high-level decision information in a separate design document. This sort of documentation is written before the code and is presented/discussed with management and peers.
That is how it was done at a couple of companies I worked at and it was very effective. Naturally, this was done at the feature level, not on a function to function basis.
The nice thing about comments is that they're in the code. You can't update the code without at least looking at them; that's more visibilty than you'll get from anything else.
Alternatively, if your org or reach for this feature is so large that you need to communicate and decide its internals with a lot of people and as such require something like a design document, then you've already failed and you have way too-many cooks in your kitchen. Design by committee is always doomed to failure.
Thirdly... if you need a design document to communicate your feature to "management" then that means you don't have autonomy to design this feature as an expert, and again, you have too many "fingers" in your pie.
Does this mean you should go into a basement and design your feature like a hermit? No, but a design document shouldn't be your answer, it should be a better team and process around you, as well as a clear business specification.
What's not so useful: mandatory comments. A public API should be thoroughly documented, but some shops insist on writing comments for every function in the code, even private ones and even if its purpose is so obvious that the comment just rephrases its name. This practice is not only a waste of time, but also insensitizes you about comments and teach you to ignore them.
Other wasteful comments are added by some tools. I hate the one that marks every loop wiht a //for or //try comment.
Do you code solo or do you work with a team? If so, how large is the largest team you've worked with?
I used to be a dogmatic "all comments are code smells" person and, to a large degree I still am. But working on a very (and I mean VERY) large code-base that is actively developed and maintained by hundreds of other software developers, I have relaxed my position slightly into the "if you need to do something weird, explain why" ... because a large legacy system that lives in a business environment of tight deadlines means that there are often weird things that need to be done to keep things moving at a pace that the business is willing to pay for.
Anyway, one of the many reasons that I argue AGAINST code comments is that the comments become part of the code and therefore require maintenance. But few people read comments unless they are stuck trying to understand something. This "psychological invisibility" is even enforced by the fact that most code editors will grey out comments in order to make them less distracting.
And therefore, comments can easily become outdated.
So I'm curious about your situation. Since you say that you like to give yourself useful context for "future you", what context does this process serve? Do you find it useful when working on a shared codebase with lots of other developers? Or is it something that only works well when there are few developers touching the code?
I am firmly of the opinion that keeping the comments up to date is part of the job of the developer. And if they're not doing it, they're not doing their job. It's no different than taking the time to write code that does the right thing, expresses the intent of what it's trying to do, etc. And, when code is peer reviewed, not including/updating comments in places where they are important should be a reason to send the code back to the developer.
I feel the same way about automated testing. They're both (testing and comments) way to make our code better; easier to understand, easier to maintain, more likely to be correct, etc.
As long as they aren't being placed every couple of lines they shouldn't really be a nuisance when reading through code.
And to me not updating the comments is the same as not updating the name of a variable when its purpose changes. The compiler doesn't care that you didn't update the name, but people reading the code do.
Also when it comes to "future you", you basically should think of yourself in the future as a different person. Because unless you are constantly working with the same bits of code, you will forget details and possibly even the high-level.
In Leo[1], you can make nodes out of them and just hide them. If Emacs weren't so good, I'd be using Leo. Similar power when it comes to extensibility, but extended via Python, not elisp.
Related anecdote: yesterday I was on a code portion of a personal project with a "Is this really useful?" comment on a line that seemed it could easily be removed. I tried to use the newer and cleaner class instead and the particular old way was indeed needed. So I appended a "=> yes!" to the existing comment as well. I'm glad my former self documented the interrogation.
At work, especially on bugfix, I often write a one or two lines comment with the ticket issue number over a non-obvious change.
You will read it in 3 months and cry in despair "But why, my past self, why is it needed?!" :-)
Oh god, that's horrible, what kind of tool does that?!
I know I had coworkers using brief configured to do that.
DEAR MAINTAINER:
This code is the way it is because of <reasons go here>.
Once you are done trying to 'fix' this, and have realised what a terrible
mistake that was, please increment the counter as a warning to the next
person:
total_hours_wasted_here = n
I'm not the original author, but have gratefully used it once or twice, and been amused when there was a single line commit incrementing the counter.I one time wrote this fairly elaborate SQL generation thing that required pretty liberal use of recursion in order to fulfill all the requirements. It ended up being a lot of mutual recursion and the code was admittedly kind of messy but it was a necessary evil to do everything asked of me.
A more senior engineer ended up taking over the codebase, "fixed" all my code to be this iterative thing, made a point to try and lecture me about why recursion is bad in an email, only for his code to not actually do everything required and him reinventing effectively everything I did with recursion.
In fairness, he did actually apologize to me for some of the comments he made, but if I had thought about putting this comment on the top maybe we could have avoided the whole thing.
It's a problem if you're overflowing the stack, of course, but otherwise…
This especially applies to your own code that you write and still have to maintain 5, 10, 15 years later. Just the other day I was reviewing a coworker's new code and thought "why choose to do it this way?" when the reason was 10 lines up where I did it the same way, 8 years ago. She was following the cardinal rule of maintenance - make the code look like the existing code.
This is so undervalued when maintaining an older codebase. Please, for the sanity of those who come after you - make the code look like the existing code.
I think it's a great rule of thumb... but there are exceptions.
For example, Funnily enough I was reviewing some code just today written by someone else and that I'm going to be asked to expand on or maintain. It looks like this:
var example1 = Object;
var example2 = Object;
var example3 = Object;
...
var example147 = Object;
And then later there are corresponding variables assigned to different objects which are: var tempExample1 = <some object instantiation>;
var tempExample2 = <some object instantiation>;
var tempExample3 = <some object instantiation>;
...
var tempExample147 = <some object instantiation>;
And this goes on and on. It's real special once we get into the real business logic. (and to be clear... "example1" are the actual names, I'm not just obfuscating for this comment.)The reason it looks like this is because the original developer copied the examples from the developer technical documentation for what they were doing verbatim; this documentation only had one variable so the numbering was the way to get multiple variables. Knowing this system, they didn't have to do that, they could have very easily assigned meaningful names for all of this. (To be fair, the original developer isn't a developer but a consultant business analysist saying, "Oh yeah, I can do that!" to the client.... billing all the way).
I can tell you with great certainty and righteousness: I'm not going to make my code look like the existing code. I may well do some refactoring to make the existing code vaguely scrutable.
I appreciate that what I'm describing is an extreme case and not really what you or parent comments really were addressing. I just stop to point out that what you describe is a rule of thumb... a good one... but one nonetheless. And, as an absolute rule of thumb about rules of thumb, there are no absolute rules of thumb. Ultimately, experience and judgement do matter when approaching the development of new code or decades old code.
Comment on whatever would be surprising when you read the code.
When I write code, a voice in the back of my head constantly asks “will I understand this code later?”. (People who just instinctively answer ‘yes’ every time are arrogant and often wrong.) Whenever the answer is ‘not sure’, the next obvious question is “why not?”. Answering that question leads you directly to what you need to write in your comment.
Sometimes the answer is “because the reader of the code might wonder why I didn't write it another way”, and that's the special case this article covers. But sometimes the answer is “because it's not obvious how it works or why it's correct” and that clearly requires a different type of comment.
My driving principle in the same vein is "where do I have to look when/if this doesn't behave as expected?" - if the answer is not in the docs (wiki -> package/module -> file -> class -> function / method) or is otherwise > 10 lines away, it gets an inline comment (or the docs are updated). Usually this happens when chopping up strings or during intermediate navigation steps over an odd data structure.
Identifiers can go a _long_ way, but not _all_ the way. I personally am a fan of requiring documentation on any public methods or variables/fields/parameters (using jsdocs/xmldoc/etc). Having a good name for the method is important, but having to write a quick blurb about what it does helps to make it even clearer, and more importantly, points out obvious flaws:
* Often even the first sentence will cause you to realize there's a better name for the method
* If you start using "and" in the description, it is a good indication that the method does too much and can be broken down in a more logical way
People often think properties are so clear they don't need docs, then write things like:
/** The API key */
string ApiKey;
But there's so much missing: where does this key come from? Is this only internal or is it passed from/to external systems? Is this required, and can it be null or empty? Is there a maximum? What happens if a bad value (whatever that is) is used? Is there a spot in code or other docs where I could read more (or all these questions are already answered)?This is stuff that as the original author of the code you know and can write in a minute or two, but as a newcomer -- whether modifying it, using it, or just parachuted in to fix a bug years later -- could take _hours_ to figure out.
Another twist on this is to put in a debug logging statement which triggers when the inputs are much larger than the original design constraints.
It's roughly the same message to a future-developer, but they might find it much sooner, short-circuiting even more diagnostic and debugging time.
Your comment is super obvious, in hindsight, but I never thought to do something like this, usually at past places of work we've just agreed to make a comment on things we need to reconsider later and hopefully we remember about that comment when things go downhill!
For example, if the slowness of the method depends on non-trivial aspects of the input, or if the performance problem in production comes from someone calling your method too many times on (individually) reasonable chunks of data.
The "et" in "etc" means "and", so you have that already.
Using a debug log-level helps prevent that problem from occurring in production, while still making it visible to developers that have more logging enabled in their dev/test environments.
I know most people don't like it, and that is fine, they can deal with it! I they don't want to see my comments, they can remove them from their version of my code with a script, and if my co-workers and boss don't like them they can remove them in a code review! However, I can say that I enjoy reading my old code way more than I enjoy reading other's code which have zero comments. I work in Python, so a lot of the simple non-algorithm code (boilerplate stuff for apps, like flask APIs for example) is mostly "self-documenting" since the old saying goes, "write some pseudo-code and 95% of the time it runs in Python." The most important comments are sometimes on the boilerplate stuff because that's where a lot of changes happen versus the algorithms where I find there is a lot more wholesale rewriting in my industry.
I will always love comments and doc comments!
The problem with unit test as documentation is that over time they end up reflecting the same misconceptions that the code has. Someone does a refactor, misunderstands how the original code works, and "fixes" the unit tests to pass. Now you have tests that lie just like comments can lie.
By contrast, I have really never seen truly self documenting code. The comments may be up to date by virtue of not existing, but the end result is just more confusing. YMMV
It was all great until you got here. This is a big red flag for me for a teammate.
Half the time I start by just writing comments explaining what I'm about to try and do, then I go back and add comments about how things did not go as expected, and what I had to do to get it to actually work. Super helpful 5 weeks later when I have to actually see it again.
You start with comments of how you want things to work, and fill in the code. It's a perfect combination of why/how and communicates to the next developer the high level thought process behind the code perfectly.
If a piece of code is weird, or slow, or you'd say "yeah, it's kinda janky" when describing to somebody, I usually write a comment about it. Especially if I've changed it before; to document some case that didn't work, or I fixed, or whatever.
When you operate on this basis, superfluous comments just melt away, and you typically end up documenting 'why' only when it's really necessary.
Try it out in your own codebase for a month and see how it feels :)
You can't have functional code for what isn't done, so that's some information you can't express in code.
Furthermore, a major problem with comments is that you can't debug or test them. There are many tools that can analyze code, static or runtime, but because comments are just free text, you can't do much besides spellchecking. Also it is common to forget to update comments, and with the lack of testing, in the end, you get lies.
But here, the only maintenance these comment need is to remove them if they stop being relevant. For example because you finally did the thing you said you wouldn't do, or just scrapped the part. Very low effort, also rather easy to spot, since if you see a thing being done with an explanation on why it is not done, it means someone forgot.
It is also worthwhile because as programs grow, they tend to do more, and "not" assumption may no longer hold (there used to be 4 parameters, now there are 10000...), meaning it is something you should check.
A lot of slow code comes from an assumption that N will be small, and N ends up being big. By the way, that's why I favor using the lowest O(n) algorithm that is reasonable even if it is overkill and slower on small sets. Because one day, it may not be. If for some reason, using a low O(n) algorithm is problematic, for example because the code is too complex or the slowdown on smaller sets too great, then comes the "why not" comment.
Interface expresses “what the function does” while the implementation expresses “how it’s done with what tradeoffs”.
Bonus, in the future, if performance ever does become a problem, you swap in the new optimized implementation behind the existing interface.
When you see a call to an interface, you don't know what the code does concretely, you only know what it is supposed to do. The actual implementation and how it ties to the interface may by in a completely different place. It is one of my biggest source of headaches when debugging code.
Interfaces are useful, the strategy pattern is useful, but overuse is harmful. My idea is to not use an abstraction unless I know I am going to need it. For example, let's say I need to decode video, and I have a hardware decoder and a software decoder. Here, an abstraction make sense, as I know the software decoder will be prohibitively slow one some platforms, and the hardware decoder won't always be supported. But if the optimized version is sufficiently better so that it makes the old version obsolete, just change the code.
And if I don't know if I will need to change strategies later, I just write the first strategy, and if it calls for an interface later, only then I will do the abstraction.
I suppose if you make the refer nces fancy enough you wind up reinventing rst
func uglyHackForCombiningTwoThings() {
// Do whatever ugly thing you want to do
}Then I realized that my languages will never be perfect, and having comments is an essential escape hatch. I was wrong and I changed my mind.
Also, 99.9% of languages have comments:
I've been in this exact situation quite a few times — use a bad algorithm because your n is low. However, instead of commenting, I did something like this instead:
function doStuff(items: Item[]) {
if (items.length > 50) {
logger.warn("there's too much stuff, this processing is O(n^2)!");
}
// ... do stuff
}Wow, someone actually suggested that?! Do people write whole programs like this?
Unfortunately, short names do not indicate that someone did know what they were doing! =)
In reality, of course, software development is about getting large groups of people in-sync as to problem, solution, and implementation. That takes lots and lots of communication. Ambiguous, messy people-to-people stuff, without The Right Solution to contents or wording. Because there's a bias towards thinking of the Program as Reality, it never occurs to such developers that an identifier can become just as outdated or wrong as a comment, and in fact it is easier to correct a comment than to globally rename identifiers, or that a 20-word comment carries vastly more information and nuance than a 20-character function name, or that trying to shoehorn information important to developers into a language used to tell _computers_ how to function is worse than trying to drop assembly language into a SQL query.
I realize you're trying to explain a way of thinking that you don't actually share—but this doesn't make sense to me either.
Computers parse identifiers in only the simplest sense—they care if two identifiers are identical to each other or different. So, any identifier longer than one or two characters is in some sense a comment, because everything longer the minimum necessary to make the identifier unique is ignored. They're literally stripped from the output if you don't retain debug symbols or use a minifier (depending on the type of language).
To me, the suggestion seems so incredibly bad that it leaves me wondering about the headspace of someone who would suggest that. Is it somehow less bad than it seems?
My biggest headache right now has been getting high-throughput with SQL and as such I've had to do a lot of non-obvious things with batching and non-blocking IO in Java to get the performance I really need, and as such a of the "obvious" solutions don't work (at least with a reasonable amount of memory). Consequently I've been pretty liberally commenting large segments of my code so that someone doesn't come in and start bitching about how "bad" my code is [1], "fix" it, and then make everything worse by rewriting it in a more naive way that ends up not fulfilling the requirements.
[1] I have since stopped doing this, but I'm certainly guilty of doing this in the past.
There's often a fairly small kernel of very dense code that abstracts away a bunch of complexity. That code tends to have well north of a 1:1 comment to code ratio, discussing invariants, expectations, which corner cases need special handling and which ones are solved through the overall structure, etc.
Then there's a bunch of code that build on that kernel, that is as close to purely declarative as possible, and aims for that "self-documenting code that requires no comments" ideal.
Finally, there's the business logic-y code that just can't be meaningfully abstracted and is sometimes non-obvious. Comments here are much more erratic and often point at JIRA tickets, or other such things.
- URL of documentation for a complex feature
- URL with a dashboard with telemetry
- URL of a monitor which checks if the given feature, CI job, GitHub action etc. works correctly.
In a big project, figuring this stuff out is not trivial, requires a lot of searching with proper search terms and/or asking the proper knowledgeable person.
I find it weird that code is often so detached from everything non-code.
I often add comments to code as I decipher it, then remove them again when I figure things out.
No matter how efficient or eloquent, if you solve the wrong problem, you’re doing no one any good.
Solve the right problem efficiently, but in a way no others can understand, and you’ll flounder in a team. You will also never be able to collaborate or make larger work move faster.
Correct problem, clear writing, then logic.
But I suspect that there's a relationship: If their writing is hard to read, their code will be hard to read.
Every function (or procedure) starts with a comment block. It first talks about the what and why. Then, a line for the inputs and another for the outputs. Next -- and this is done closer to the end of the writing -- I describe what it calls and what it is called by. The comment block optionally finishes with room for improvement.
The function itself probably has other comments. Usually for anything which is not blindingly obvious. Because I write code like a caveman, wherein only one thing happens on one line, most everything is quite clear. If there's anything weird or magical that has to happen, it gets a comment.
Elegance and cleverness is reserved for data structures, algorithms, and so on, rather than doing a lot of stuff in as few lines as possible. I do this for Future Me, who might be having a bad day, or for anyone who wants to adapt my code to something else.
One of the last steps in a finished program is going through and making sure that my comments match my code. I am a very boring kind of programmer.
In other words, the comment allows the author to reach into the future and co-debug with the reader, even if the author is no longer there.
People rarely touch what I write, but if they do, and they want to strip the comments out, thats totally fine with me, just don't ask me how it works after you do :P
He did not believe in comments, much. I think he thought he was commenting the hard stuff, mēh, I am unsure.
It has led me into strong opinions.
* Document every function. The function should make clear what the preconditions, preconditions, and the purpose are
* Docunebnt every file/code unit. Why it exists
* Document important loops like functions
* Document the easy stuff. It is not easy if unfamiliar
* Review the comments when working on code
This would have saved my company about thirty percent of my time.
The compiler does not verify comments, like it does code, so it is a burden. Bad programmers get another opportunity to sow chaos, I know. But one of the main purposes of code is communication with following humans, as well as controlling machines
Careful and thoughtful comments are a professional obligation IMO
Doesn't seem to be a problem here though because they're replacing macros by symbols that are known ahead of time.
For instance, you might write something like:
# I used a bubble sort instead of a quick sort here because
# the constraint above this guarantees there will never be
# more than 5 items, so it's faster to use the naive
# algorithm than to implement a more complex algorithm that
# involves more branching.
or # Normally we'd do X, but that broke customer Y's use case
# based on their interpretation of our API docs which we
# had kind of messed up. So now we do Y because it works
# under both interpretations, at least until we can get
# them to upgrade.
Basically, tell your audience why you're not using the expected method. It's not because you didn't know about it, but because you do know and you've determined that it's not a good fit for this use case.Likewise, the title "World's longest DJ set" was confusing, because most people will assume that the compound word is "DJ-set". But if you read the whole article, then you realize that a python snake fell on the mixing board and accidentally mixed some tunes. So the compound word was actually "longest-DJ" -- a 2.5 meter python.
We should all consider using hyphens for all compound words.
We do use hyphens in English. Well, some of the time, and some of us. I could be wrong, but I do feel that, given my age and also my readings of older texts, that the use of hyphens in this way has become less common, and that this was much more common decades ago to avoid ambiguity.
Because the article is a rebuttal to those who oppose writing comments (that is where "why not comments" means "why you shouldn't write comments") where the main argument is that you should write "why not" comments (that is "why not comments" means write comments explaining why you didn't do something or do something a certain way).
I feel like I’m getting off the self-documenting code ride. In our own codebase we rely way too much on “descriptive names”. Like full-on sentence-names. And is the code self-documenting? Often not. You indeed cannot describe three or more axes of concerns in one name.
Do comments go stale? Well why does it? Too loose code reviews? Pull requests that have fifty lines of diff noise that you glaze over? We have the tools to do better on that front than some years ago at least.
It’s a joy to find a corner of the code base where things are documented with regular sentences. Compared to having to puzzle through five function call layers.
[1] But yeah, really. But also: sometimes also in comments. Sometimes both.
Ideally the future user would trace the git blame back to the original commit of they really had questions.
A long comment is really helpful sometimes though. I like to put ascii truth tables for complex boolean logic, both to ensure I cover all cases when writing the code (tests, too) and to make it easier for future me to understand what's going on at a glance.
def fetch_data(comment_id):
Args:
- comment_id: The id of the comment to fetch.
Returns: The comment data
# Fetch comment datadata = fetchCommentData()
I worked on such project in my first job and I cared a lot about my commit messages, and changelogs.
On the other hand, in fast paced corporate development I barely ever see someone make commit messages like this. Even PR titles often leave a lot to be desired.
Who is arguing this? Usually, if I'm adding a comment on why something is done a particular way, it's something that is going to take at least a full sentence to explain if not a whole paragraph.
Sure people can tell what the code is doing over a file but comments could definitely add a little more context of what the code is _for_.
It seems only to be a thing with Jupyter notebooks, and even there it mostly describes the results and not the code.
It makes it way easier to send these into Claude (it seems, at least). I hope they introduce a semantic/vibes search too as I can never remember what I name my classes and functions...
The problem with comments is that they can become stale, and it's often possible to self-document or write simpler code that causes less surprise. But of course, it's totally fine to put comments.
And I think comments should be mandatory for interfaces functions/types unless their behavior is obvious. I don't want to read the code to understand what a function does, or what invariant a class maintains. And if it's too complex to document in a few lines, probably this isn't the right interface. But apparently, this isn't obvious for everybody. In my company, most of the code isn't documented.
I nearly exploded trying to grok this
Or you know, you could have just used a hyphen instead of clickbaiting.
I admire the honesty, but will continue to phrase these "why not" comments as insincere TODOs.
Oftentimes, the reason for that is that 20 years ago stuff like Doctrine, Liquibase or whatever just didn't exist. You know, the time when PHP developers shipped straight mysql_query calls with direct interpolation of $_GET, and most "enterprise" Java application came with a ton of SQL scripts and a dedicated multi page UPGRADE file explaining in which order you had to run the schema migrations, reboot systems, run manual migration scripts and whatnot to get an upgrade done. Some times, upgrades could literally take days.
Naturally, people invented their own stuff to make stuff just suck a little bit less, and it got more and more used in a company, only ever extended in functionality... the dreaded "corpname-utils" JAR dependency (if you're really unlucky, the JAR having been semi-restored from a half-broken decompile because the sources got lost along the way) or util.php that just got copied over from project to project. And that's how you end up in 2024, still maintaining some ORM that has its origins in Perl code written in the 90s by someone deceased in the '00s. (Yes, I've been there, although not that bad)
When commits are rebased, the log message must be revisited and revised. Changes can disappear on rebasing; e.g. when a change goes into a baseline in which someone else made some of the exact same changes in an earlier commit, so that the delta to the new parent is a smaller patch. In my experience, commit messages stay relevant under most rebasing.
Comments are (largely) an obsolete version of version control log messages.
In the 1980s, there was a transitional practice: write log messages, but interpolate them into the checked out code with the RCS $Log$ thing. This was horrible; it practically begs for merge conflicts. It was understandable why; version control systems were not ubiquitous, let alone decentralized. You were not getting anyone's RCS ",v" file or whatever.
Today, we would be a few decades past all that now. No $Log$ and few comments.
Mainly, the comments that make sense today are ones which drive automatic API documentation. It would not be reasonable to reconstruct that out of the git history. These API comments must be carefully structured so the documentation system can parse them, and must be rigorously maintained up-to-date when the API changes.
I could easily imagine somebody refactoring code to an obviously better version, finding out it doesn’t work for subtle reasons, running got blame and cursing the person who left that information in a commit message instead of a comment.
If that obvious incorrect way happens to match what you're thinking of doing, then that kind of why-not documentation will help.
Warning future developers not to make certain changes is indeed something that should go in a comment. We can make a short comment like, /* if you think you can rewrite this in a simpler way, read commit df037acb first */
There are unfortunate situations when things have to be changed in multiple places together; you must not forget all of them. That deserves comments.
If you have an enumeration that has to pack into 3 bits, there should be a comment warning not to add more elements than eight without increasing the number of bits; especially if there is no compile time assertion for it.