Linus Torvalds' good taste argument for linked lists, explained
github.com
github.com
remove_list_entry(entry)
{
prev = NULL;
walk = head;
// Walk the list
while (walk != entry) {
prev = walk;
walk = walk->next;
}
// Remove the entry by updating the
// head or the previous entry
if (!prev)
head = entry->next;
else
prev->next = entry->next;
}
remove_list_entry(entry)
{
// The "indirect" pointer points to the
// *address* of the thing we'll update
indirect = &head;
// Walk the list, looking for the thing that
// points to the entry we want to remove
while ((*indirect) != entry)
indirect = &(*indirect)->next;
// .. and just remove it
*indirect = entry->next;
}Reminds me of the editor(s) who helped "fix" bukowski's poems
Similarly, I’m not a fan of everything Linus wrote, but I wouldn’t enforce bad CS101 code on him so that little Jimmy could read it, and I like Linux.
It just sort of means any below-average person.
Adding "little" in front just stresses the fact that we are talking about a kid, so the references are about dumbing it down for kids.
Johnny is probably a bit more common than Jimmy, but I've seen both. See e.g. [1]
https://www.pbs.org/newshour/arts/poetry/bukowksis-poems-wer...
Usually variable names in a loop is the object being manipulated not the address of that pointer which means p isn't a pointer to the object which makes it completely confusing.
If the article used pp which is the typical way you describe a pointer to pointer it'd help
(Granted, the code in the original TED talk is broken, as it doesn't define "head".)
A linked list in C isn't a struct with a "head"; "head" is just a pointer pointing to the head element.
The OP article should be retracted.
But it should be retracted? Really?
Would you find the comments in the second version helpful, or should they also be removed?
Edit: let me lay my cards on the table. If the comments really are necessary, it doesn’t seem elegant. I’m pro-comments, but that’s because not all code can be readable and elegant all the time.
So I'll disagree with the notion that only illegible or unreadable code needs comment.
I've been C/C++ for about 25 years.
So at a function level the comments is giving a highlevel description to the reader as to the functionality contained, and at the line level comments exist only to say describe the programmers intentionality, why it was implemented this way rather than some other way.
I have generally found (there are always exceptions) that if you find yourself needing "what" comments within a function you should be considering breaking it into smaller pieces.
I've been being paid to write c/c++ for about 25 years
The particulars matter.
// indirect is either &head or &(node->next), so *indirect is equivalent to "next node"
Someone who knows their C should be able to figure it out in a few minutes by just reading the code. remove_list_entry(entry)
{
for (p = &head; *p; p = &(*p)->next)
{
if (*p == entry)
{
*p = entry->next;
break;
}
}
}
However, what the choice does objectively impact is performance. Being able to assume the object exists allows you to avoid checking against NULL, which removes instructions in the loop. I'd guess they anticipate this code might be called from performance-critical code, and that might be why they coded it like this.Branch predictors will probably do pretty well on this, but still.
Two terms that would not fly in kernel level programming; with kernel programming, low-level libraries, you want both optimized speed and predictability.
That's a really broad statement. Arrays can get surprisingly big while being faster. Performance-wise, you'd usually want to use arrays up to a certain size and then link between entire arrays. I'd say the biggest advantage of a standard linked list is that it can be intrusive.
That's one big benefit. I teach operating systems and, when the semester has been good, we dig into physical memory dumps with a few python scripts, and show the students how to parse Windows process list following _EPROCESS structures.
That's one of the big things they find themselves surprised every year: that the process list is a chain of LIST_ENTRY structures embedded within the _EPROCESSes[0] (or any other structure that is doubly-linked in memory). And LIST_ENTRY is just 2 pointers going hand in hand with list_entry->flink and list_entry->blink attributes and nothing more.
[0] You can read some about this structures here https://www.nirsoft.net/kernel_struct/vista/index.html but beware this is based on Vista and a bit old. Kernel data structures vary with versions of the OS (and not even major versions at that) and target processor (x86, x86-64, ARM too would be reasonable).
The purpose of the OP and the Linux's talk is to show a better way to walk through linked list, not to show a full implementation.
In most cases you use a linked list like this, you don't care about the order of things, just that you can iterate over all items in the list.
For example if the linked list was:
&next: (100, 0xFFFF)
0xFFFF: (101, 0xDEFF)
0XDEFF: (102, 0)
Then calling remove on head would leave us with:
&next: (101, 0xDEFF)
0xFFFF: (101, 0xDEFF)
0XDEFF: (102, 0)
(It would be on the caller to free the memory address 0XFFFF)
// The "indirect" pointer points to the
// *address* of the thing we'll update
The "indirect" pointer points to the thing we'll update. See at the bottom, it's updating *indirect, so "indirect" points to the thing being updated. On the other hand, "indirect" points to the address of the thing we'll remove. There's a specific item being removed, and there's a specific thing that will be updated; the thing being updated comes immediately before the thing being removed; they're not the same thing.I don't think so. I think the 'indirect pointer' points to the previous 'next' (or the head). It doesn't point to the address of the previous 'next' (or the head). What you say is adding an additional level of indirection that doesn't exist.
Reality:
indirect -> previous next -> first element
What you're saying:
indirect -> address of previous next -> previous next -> first element
In reality indirect contains the address of the previous next, but it doesn't point to the address of the previous next.
indirect -> previous next -> first element
"indirect" points to "previous next". "previous next" isn't a list entry. "previous next" points to "first element", which is a list entry.
What does "holds" mean? If holds means actually contains the memory, then I don't see the distinction between "thing" and "entry". They both refer to the exact same piece of memory.
while (!!walk && walk != entry) {
prev = walk;
walk = walk->next;
}
or while (!!indirect && !!(*indirect) && (*indirect) != entry)
indirect = &(*indirect)->next;
...because without those checks it's pretty easy to see where you'll crash... but by putting in those checks, perhaps the second method might actually be less 'elegant'.Edit: did I miss something? Re-checking this, the second method will still crash if/when next is NULL (because you can't get the address of NULL), which means the 'elegant' seems to be a quagmire of lurking faults.
At the very least, the code should add comments on the the assumptions that are vital...otherwise n00bs will copy the code as gospel and run into all sorts of problems.
It was my experience when tuning a linear algebra library (just for internal use) that such comparisons are almost unobservable in total performance tests.
This is very different from kernel code, which by its nature isn't very computational but doing resource management all day long and pretty much all it does is stuff that looks like walking linked lists.
This was around 2001-2002 anyway. Things might have changed.
Depends on how often you are doing the comparison. Is it 1 time/second, or 10 million times a second?
There is a difference.
If the code above is the one in charge of putting/removing network packets in a queue, or putting threads ordered by priority for the scheduler, then you should consider the side effects of checking for NULL.
If you are going to implement this function in a library for Jimmy The Programmer[1], then check for NULL.
Me, though? Well, let's just put it like this: when we find some framework-level bug that's written in "clever" ES6 syntax, our first step in debugging is almost ALWAYS to rewrite the given function traditionally, without any of the ES6 shorthand. And the reason we do that is because reading and debugging a whole stack of anonymous lambda calls is a PAIN IN THE ASS. Or figuring out where a certain variable is coming from when someone uses overly-complex destructuring syntax to magically pull a value from deep within a nested object.
I mean, don't get me wrong, I do like and use almost all of the modern ES6 niceties, but I also feel like it's much more difficult to parse and understand code compared to what we were all writing a few years back. People will, I'm sure, be arguing about what constitutes "good code" for decades to come, but to me, when working in an evolving codebase, especially with other people, plain ol' human readability is paramount. If people can't figure out what your code is doing without throwing in a breakpoint and stepping through line-by-line, you've failed at writing good code. And this will be my opinion right up until the day humans stop writing code by hand.
while (!!(*indirect) && (*indirect) != entry)
indirect = &(*indirect)->next;
Also I would rather use * indirect instead of !!(* indirect).I still use it as a clarification that it's a deliberate boolean operation, rather than implicit.
In C any nonzero value is considered "truthy", and on most architectures NULL is defined to be (void * )(0) or similar. The logical not operator AKA bang operator will replace truthiness with 0, and falsiness with 1. So applying it twice collapses all nonzero values to 1.
If you call that method and the element doesn't exist, presumably something has gone wrong already. Adding a NULL pointer check is not going to fix it. You just silently ignore the error. You'll prevent the crash, but there's no mechanism for handling the error.
I can fail so you can catch it, or if you don’t catch it, you’ll at least see exactly what went wrong. You expected the item to be in the list and it wasn’t. Your preconditions were wrong from the beginning and you have an (unknown) bug in the code.
The alternative is that I can’t find the item and I silently suck it up and don’t notify you. If that’s the api we’ve all agreed on, that’s fine. It was on you to check for the thing in the list first and then remove it. But it’s a bit weird if you think about it - now you need to traverse the list twice. Once to check for the existence (and fail out yourself) and once to remove.
next?.doThing()
Where doThing is never called if next is null.Languages that do this use "scary" operators to crash on null:
next!!.doThing()
And it's drilled into people's heads the latter is a Bad Thing (tm)-
You really need to consider context in null handling.
Imagine an app that alerts a nurse when the patients heartbeat is out of range.
In an application where the UI context might have been closed out, it's common to see
someUiContext?.showAlert()
But what actually happens if the context is gone?It's better to crash and have part of your startup procedure be communicating that a crash occurred, and the doctor should check that something went wrong, than silently continuing.
-
The problem is when you tell people this, the kneejerk reaction is always "are you saying we intentionally add crashes"!
Because safe null handling was specifically added to avoid the situation where that crashes...
(For example, if the app was a news reader and the alert was "Article failed to load", you wouldn't want the app to crash just because the user left a certain page before the alert was shown)
But I think the pendulum has swung too far at this point, people are so used to just sweeping nulls under the rug, and it's not great for finding issues
Sure you could make the argument that we don't _know_ they are being checked, but it's a pointless discussion. Who cares? _If_ preconditions are met, this code is safe, if they aren't, it's not safe. Since we don't know one way or the other, there's no point in discussing further. The kernel developers know their stuff...
From a black box, zero-knowledge, perspective it’s maybe worth remembering that code built this way successfully runs mission critical systems all over the world every day. Thoughtful people would have switched to other (slower, more pedantic) systems if doing things differently was a real overall life improvement. People have had the choice, there are plenty of OS’s out there... some far more formal/pedantic. Linux wasn’t the safe choice back in the day, it was just better by several important metrics consistently over time.
Perhaps these insanely experienced kernel devs know what they are doing.
Imagine if at every function down a complex stack you go with:
if (!ptr1 || !*ptr1 || *ptr1 > 5 || param1 < 0 || param2)
/* etc.... */
{
return NULL;
}
(used arbitrary names and values).I'm sorry if this might be common knowledge, but I've honestly never heard this and can't understand it.
Is it saying that it's better to do something than to (formally) prove it, like the opposite of (I think) Knuth's famous quote?
"Premature optimization.... " you get the drift.
IMO, even though this is C, the Zen of Python still applies, which states:
> Errors should never pass silently unless explicitly silenced.
Its the caller with the bug, not the library code.
Then the type of the function would just declare a that the pointer can't be null, and code which sends a nullable pointer would refuse to compile.
Make it right
Make it fast
This is an ordered list, and the people who forget that make a lot of work for the people who don’t
It's definitely not right, while probably not really working either. The immaterial performance gain from removing the null check is completely irrelevant.
It's a foot-gun for sure, but it's up to you to not pull the trigger.
There are several comments indicating the performance cost of a NULL check. Go and look at the disassembler and see the code. You should find that it equates to JZ, which at worst case costs 1 clock cycle. Am I incorrect?
So, another comment below mentioned 10 million deletions per second. Ignoring the fact that you should be using a double linked list at that point (like the kernel does), in a single threaded 2.4GHz CPU a 1 step opcode (JZ) equates to a bit over 4ms/sec. With branch prediction, I expect to be far less.
So, the NULL check is almost free. Even on embedded systems, it'd still be a good idea to keep it in, to insulate yourself from flipped bits (eg, solar flares, or degrading memory) and coding mistakes elsewhere.
In these days of security research, I strongly advise for defensive code: it means that each function has had it's edge cases thought about, which means the developer has spent time thinking about the stability of their program, which cannot be a bad thing. Don't make your code a deliberate point of exploitation.
I guess my question is, what's wrong with crashing, in the case of a real screwup, where something has gone horrible wrong, assuming you have a proper crash handler?
But for general purpose code, e.g. libraries where performance isn't super important but stability is, more defensive programming would be a thing to do.
this line has 4 spaces in front of it.
What Hacker news could use instead are quote blocks. Now people often use the code blocks as quote blocks. This has no superfluous indentations.I think Linus is correct on this one.
I'm ok with clever code when it's explained in words.
The idea here is to make the "p" pointer point on the "next" field of the current element instead of at the structure itself. There is the same amount of indirection in both solutions.
The first solution does pointer->structure->pointer, the "elegant" solution does pointer->pointer->structure. The reason the first one may be easier to read is because the C syntax "prefers" it, as it is a more common construct.
The latter approach is definitely not the easiest to understand. It is, however, what Linus considers «good taste in coding.»
That's neither here, nor there though.
The simplicity of a concept does not directly translate to how hard it makes the code (assembly is simpler in the sense of having fewer and simpler constructs than e.g. Python but much much harder to code in).
Conditionals, and the exponential increase in state space they bring, are still one of the biggest source of errors and complexity in code.
They are expensive! Your code maintained years in the future, every developer has to potentially read every damn line.
The alternative to "clever" code isn't Java and FactoryFactoryFactories, it's short clear code, which is different from short codegolfed code. The main difference is nicely designed libraries and abstractions.
https://old.reddit.com/r/adventofcode/comments/k7ndux/2020_d...
These are self-selected people, free to use a language of their choice. Ask yourself some questions:
- Which of these do I think is "most readable" vs "least readable"?
- Which of these am I happy is correct, and bug-free as far as it goes?
- Which of these would I like to make a change to, confident that it won't break?
- Which took the author most / least time?
- If I could only pick one to run on my puzzle input and submit the one answer which came out, which of these would I pick?
- Which of these would I like to maintain long term?
Another way to see the same ideas is to pick a task on RosettaCode - https://rosettacode.org/wiki/Category:Programming_Tasks - and see how much / little code people write in various languages to solve the same problem.
IMHO it isn't "the longest one" that is clearest, by a long shot. Nor the golfiest one. But it tends to be the shorter ones done in languages that have higher levels of abstraction, more libraries, nicer looking naming, more standard patterns.
Potentially, using a code-golf implementation where you're using pointer-to-pointer to get rid of a single, easily understandable "special case", is more "expensive" in coder-time than just using the standard off-the-shelf linked-list that (even Linus admits) everyone knows from their data structures 110 class. Because now every developer who ever reads that bit of code for years into the future, now has to understand your code-golf solution, how it works, why it was done, and the implications for the rest of the code. Whereas they could probably skim the "standard" solution and say "yes, that is a standard linked list" and move on to solving the actual problem instead of trying to understand the code golf.
In many cases: your clever solution isn't solving a difficult enough problem that it's worth the cleverness, because cleverness is often expensive.
edit: I agree elsewhere that this is probably a question of "vocabulary" and whether a particular bit is "clever" or "code golf" depends on your particular team.
Generally less code is better, since there are fever lines that can have a bug.
While pathological terse code can be obscure - to prove that the author is clever - and then it really is a pathology - simple terse code is just beautiful.
The one Linus doesn't like likely runs faster (at a microarchitectural level), it's also the thing I've done for 40 years now, it's what comes out of my fingers when I code linked lists, for me at least it's more understandable because I already understand it
theDependentVariable = theCoefficient * theIndependentVariable + theIntercept
it is much harder to recognize than if I write: y = a*x + b
So, longer variable names might be "autodocumenting" but they also make code harder to read.It isn't safe to trust that to be true. That way lies bugs, including security holes. You have to carefully check the names.
EthAccHdlrSubsMacPhysRegWrite and EthAccHdlrSubsMacPhyRegWrite
Shorter is better, within reason.
Surprisingly, I ended up in a different spot.
I actually think this is a low-level C-version example of the best practice of "use idiomatic language concepts".
When C# first got LINQ, when Java first got Streams, and when both got inline anonymous Lambda functions a lot of old-school developers resisted both.
"The syntax is confusing!". "The behaviour is hidden!". "What's wrong with a good ol'd for/while loop?"
I know because that was my first instinct too.
But I quickly embarced these ideas because they allowed me to write more terse declarative code and spend a larger percentage of my TLOC on business logic. There was a small learning curve to new developers to understand the libraries, but after that everyone was more performant.
I feel the same way about C and pointers and address manipulation magic. No, it has no place in Java or C#. But for C developers, these are their idiomatic principles and concepts. Pretending otherwise, and not leveraging all the capabilities possible with direct pointer and address manipulation is not utilizing the benefits of C to it's full potential.
NOTE: I am not a C developer. This is just how this comes off to me. I'd love to hear from actual C developers if they would say that this is the equivalent of running around with a loaded shotgun and languages like Rust have been designed with solving these things in mind (or something).
I write C full time, and have done so for many years. It didn't occur to me that the pointer to a pointer aspect was the source of any of the confusion until just now. Actually, I don't think that I consciously registered its presence when I read the code. There is only one interpretation of the code that makes any sense, and that's at least easy for an expert to see quickly.
I am sure that people that think that the terser variant is overly clever don't get it. To be fair, it's hard to generalize from this one example. But I think that I know exactly what Torvalds intended to draw attention to.
The example here is a good one. The shorter version reads much better and is more obviously right.
I think they are infinitely worse than while loops. (I ran into this specifically with Pandas dataframes/series)
You save 1 or 2 lines and have ambiguity on wherever "each" is.
Maybe static typing solves this, but I've decided I hate syntax sugar and dynamic typing.
What is "each" ?
It also splits apart two different kinds of criteria. "Has this run ten times" is a different sort of question than "is the error within the given margin" or "does the temperature now read 60 degrees". (At least, if you're intentionally running the thing 10 times. If it's running incidentally so you don't even know if it will run 10 times then it can be the same sort of question, but in that case I'd argue you should use a while loop….)
> You save 1 or 2 lines and have ambiguity on wherever "each" is.
Like the sibling poster, I'm not certain what you mean by this.
But I see this as a paradigm that you can apply in more programming languages: you should aim at writing code that removes edge-cases. This is not about code reduction in my opinion, I would have been equally happy if the code had been longer. It's about more secure code: because there is no edge-cases to think about.
But this particular finesse isn't necessarily about extra cleverness. Here the situation is that all the linked-list nodes look the same and can be/should be treated the same. The reason the first bit of code has to treat "head" differently is that head is the only node that the rest of the program keeps a pointer to. But that should be incidental, by doing the operation indirectly, you do things uniformly, you should do things uniformly, since the linked list has a uniform structure.
Here, it's not much about combining cases overly cleverly, not leveraging "while (x=y){...}"-type stuff but removing what is an unnecessary kludge; that you have to treat head differently 'cause it's the value you happen to have and you need to update it while preserving it.
Think of how often the code will be executed; this particular code, or kernel level code, will be in the order of millions in a regular working day. Most code that I for example write will be tens, maybe hundreds; it's mostly glue code between a database and REST api for a relatively low-traffic internal management application.
But it runs on a Linux machine which, for every HTTP request between the browser and my back-end, will run thousands of lines of code and iterations (I guess?). I really don't mind if the code looks clever if it means it's ten times faster than the readable code.
Besides, clever does not exclude clarity.
Except that it actually isn't uniform, is it? A linked list has three kinds of nodes: the first node (to which nothing points), the middle nodes, and the last node (whose next pointer is null). Nulls vs populated values really do change the shape of data. Any code to handle a list needs to be well aware of those three cases, even if there's no IF statement for them. I would argue that having code that is organized around these actual differences is better than trying to normalize those differences into a single thing, at which point those meaningful differences risk being overlooked in code maintenance. At a minimum, this kind of code needs to really clearly explain why it is doing what it's doing in comments, and how the multiple cases are handled in the streamlined solution.
void remove_entry(node_t *entry) {
// curr_ref is the address of the link pointing to curr
node_t **curr_ref = &head;
node_t *curr = *curr_ref;
while (curr != NULL && curr != entry) {
// Advance curr_ref and curr
curr_ref = &curr->next;
curr = *curr_ref;
}
if (curr) { *curr_ref = curr->next; }
}
Choosing names somewhat more rationally, and making it clear that "curr" always points to the current node, and that "curr_ref" is the address of whatever pointer we followed to arrive at curr, makes it easier to establish the invariant that updating curr_ref is sufficient to insert or remove curr into a list, no matter if it's referring to the head link of a list or the next link of the prior node.Now, on a different note, I am a bit puzzled because I don’t see a free(*ptr) call in Linus’ or anyone else’s code. The code, as-is, would cause a memory leak.
There’s a need to capture the curr_ref before it’s overwritten, and free it after it’s overwritten.
So generally, list implementations in C will not free the node, only remove references to it and return it.
[1] https://www.cs.utexas.edu/users/EWD/transcriptions/EWD07xx/E... and https://www.google.com/search?q=site%3Ahttps%3A%2F%2Fwww.cs.... for more
Pointers and addresses are pretty fundamental to C, but many C programmers only use them in certain fixed patterns: An address is what you get back from malloc() and pointers are variables to heap-allocated objects. Or pointers are the type you use for parameters when you don't want to copy the value.
There is a deeper understanding you can have: pointers make storage locations first class entities. A common refrain in programming is that if you want to increase the expressiveness and flexibility of your code, you make some concept first class because then you can abstract over it.
In C, storage locations are first class because you can take the address of anything. This lets you abstract over which storage location an operation modifies. Linus's "trick" relies on understanding that using a pointer (to a pointer in this case) lets you abstract over where the node pointer is the head pointer or a next pointer.
If you have Linus's mental model of C, what he's doing isn't clever. It's smart. It uses a fundamental concept of the language to avoid error-prone control flow and edge cases. But if you're only used to using pointers within a handful of proscribed patterns, it likely seems very strange.
I won't make any judgements as to whether thinking of C in this way should be something that people do. Anecdotally, a while back I wrote a blog post on writing a garbage collector from scratch: http://journal.stuffwithstuff.com/2013/12/08/babys-first-gar...
The mark-sweep algorithm isn't super sophisticated, but this is fairly tricky low-level stuff. A lot of people have read it over the years and basically the only negative feedback I've gotten is around where I use the same pointer-to-a-pointer to remove from a linked list. It confuses a lot of people.
So, in my own personal code, I'm fine with stuff like this. But when coding with others, I tend to avoid it.
> We can think of a functor as a container, which contains one type of item. ... > A pointer: *T is a container that may be empty or contain one item;
Despite that comparison probably making seasoned FPers groan, that was a big clicking moment for me.
Thinking along the lines of nullary-function:pointer::return:dereference, frames Linus' abstraction in an interesting light. You're manipulating the "function which yields the struct", not the struct itself. In fact it looks much closer to a map than the cs101 blurb in that now all nodes can be treated symmetrically.
[0] - https://awalterschulze.github.io/blog/post/monads-for-goprog...
I'm not a C programmer, and though I do have a basic understanding of pointers, I still find them rather confusing because of the indirection they introduce. I could follow the first example just fine (because I know how to deal with linked lists from Lisp), but the second gave me rather a headache (two levels of indirection!).
However, I think that once you've really understood the concept of pointers - and I'd expect that from all kernel developers - the second example really wouldn't be any harder to understand than the first. And yes, then it is the cleaner way of doing things, and therefore to be preferred.
If a beginner C programmer reads this code, they might not understand until they have a little more experience under their belt. But I think that's ok; if we limited ourselves to writing code that beginners in the language can easily understand, we're going to miss out on a lot of important, useful techniques.
I do absolutely agree that cleverness should be avoided. We should optimize for later readers of our code. But I don't really see this as falling into the "clever" camp, at least not in way we mean "unmaintainable code".
block
prev = cur
/block
if(prev)
It immediately tells the taste that's bad. Especially for ones who come from writing expression rather than statementI don't think this is really about taste. It's about "common ground" between programmers. If you've seen and used the second pattern many times before, it becomes part of your vocabulary. You're then able to express yourself more succinctly. It is then obviously a better solution to you.
If you see another programmer use the same pattern, it is common ground between you, and you like it. As long as every or most programmers on the team share the same common ground, you are more effective for using it. If you don't share it, you're less effective.
Everyone here commenting that they like the first solution better probably doesn't share the necessary common ground with Linus.
The big question is: what common grounds should you expect when writing your code?
They are good tools (especially in codebases shared with people who are allergic to loop statements!)
A good old loop in code bases that are not afraid of them are totally fine and probably clearer in many cases.
but it seems to cause friction and get called out whenever I use it in a group environment. so I don't. or I change it.
the code itself isn't important. its about the functioning of the group, the velocity, and the ownership.
It isn't either/or. It can be all of the above, to varying degrees, that change over time.
Someone's who finds it difficult to learn or to use it may not be ready for the kernel development. So it works as expected on more than one level.
The same goes for intrusive containers and few other things that many people learn in school, but get at first confused by in practice. If this creates a friction at the group level, then it's the problem with the average group skill rather than the code. And it's the former that (ideally) needs addressing. Just like you wouldn't use bubble sort because the "group" has trouble understanding the alternatives.
Working with such a large piece of code as the kernel, it's invaluable to have a certain sense of shared ideals how the code should read, and it's probably a good thing to have maintainers that nitpicks about such details.
People sometimes say things like "just a matter of taste" as if that somehow made it less important, but taste is probably the most important trait we can share when programming.
The first one -- I read it, I know what it does, it seems intuitive to understand, and I expect it to be bug-free especially because the edge case is explicitly accounted for. If someone else has to modify it later, I'm not particularly worried they'll mess it up.
The second one -- it took me about 4x longer to understand what it does. It works too, but it doesn't match how my brain naturally thinks about it, so it leaves me with the uneasy feeling "what if there's a bug?" and I'd be more worried someone else would introduce a bug if they had to modify it later.
I don't want "elegance" or "good taste" in code. Unless it has a good reason to be hand-tuned for performance, I want code that is written the way you'd expect an average programmer to write it. Nothing clever, just a straightforward translation of requirements into code. Not "transforming" them into something more "elegant".
The second solution takes me a bit longer to wrap my head around, but once I do, I like it more, because there's fewer conditionals, and so there's less chance that the function breaks on unusual conditions.
Using fewer local variables is also a nice plus.
His example, I think, can be extrapolated to explain the design of Linux’s “clone” system call for threads, which creates a new process that uses the same virtual address space as the parent process, with a different stack location; but more importantly, those “threads” are scheduled by the OS’s scheduler like any other process is. I’m unaware of any other OS which implements threads this cleanly.
From his talk, “Sometimes you can see a problem in a different way and rewrite it so that a special case goes away”
https://eli.thegreenplace.net/2018/launching-linux-threads-a...
I believe this comes from Plan 9.
I would much rather extend the latter than the former. (Though it could be I've used a similar solution in the past.)
I suspect this might a case similar to that of the "Waterfall Method", where we have this unexamined belief that in olden times or academia they just weren't capable of comprehending really obvious things. CompSci instructors, and most of their students, are quite capable of recognizing that two pointers aren't needed, if only because every algorithms textbook they've ever read discusses it.
I think that this might be like Bubble Sort: it's discussed in class as a way of illustrating a point. In students' fuzzy memories of school, they remember that bubble sort and 2-pointer traversal were taught, but they forget that they were taught only to show why selection sort and single pointer ("look ahead") traversal are better algorithms.
As a side note: I don't like the "elegant" version's use of double indirection. It's not necessary if you return a pointer instead of void, which is also nice because you can treat calls to your list functions as lists themselves and chain them together. It also allows you to avoid warts like `p = &(*p)->next;` at the modest cost of needing `p=remove(p,t)` instead of `remove(p,t)`.
Finally, the use of two structs is unnecessary. IntList is just a wrapper around a pointer to IntListNode. Why not just use a naked pointer to IntListNode like the gods intended?
So Say We All.
Agreed.
> It also allows you to avoid warts like `p = &(p)->next;`
Does it? Isn't having `p = &(p)->next;` in the while loop necessary?
> at the modest cost of needing `p=remove(p,t)` instead of `remove(p,t)`.
I may disagree since an API is used many more times than the API function is written.
` p=p->next
As for the API, returning the pointer allows a function to be an argument for another function, which I find more flexible. ` p= remove(p,find(p,value))//
I also like the option to be able to do stuff like:
` p=remove(remove(p,t),t); //remove two
or:
` if (!remove(p,t)) {//that was my last node }
But that may be an artifact of my preference for functional programming.
remove_list_entry(entry)
{
next = entry->next;
entry->data = next->data;
entry->next = next->next;
free(next);
}
You do need to handle the case of deleting the last element in the list. That can be done by keeping an EOL element at the end just for this purpose. The real caveat is that it breaks external references to list elements. If you are managing the list yourself however and can deal with these issues you can avoid list traversal (or save a pointer on every list element vs a doubly linked list). Just something to keep in your bag of tricks.We can prove using proof by induction that Linus's implementation works because of the base case "indirect = &head", and the inductive step which is the rest of the program.
It'd take a lot more work to mathematically prove that the branching program "works" due to the if statement -- you'd have to construct a proof of each branch and prove that the combination works.
I'm not a mathematician, but this was a major point in CS courses at university. Being able to prove an algorithm works can extend into automated program checking / auditing, etc.
It's not just "taste" -- there's a lot of practical theory to be applied.
Arguments on good taste will always finish in ego fight when people disagree.
The fact that we have all looked at code and not known WTF is going on and stepped away and come back a little later and it was obvious should be all the evidence needed not to to assume too much about what some other programmer will understand just by looking the code itself, especially elegant code.
However, given that we're talking about Linus Torvalds' opinions on what makes code 'elegant' or 'good taste', I'd say it's implicit that we're talking within the domain of low level, high performance code, such as kernel code.
So I think the image with the blue boxes is misleading and it should be
->[4->[12->[3->[6->[2->[]]]]]]
It is now obvious that you can always point -> to a different [...]
And as it turns out, that is basically the Cons/Nil view of a list from functional programming or lisp, if you're so inclined. And in those languages you would pattern match on the constructor once and do the tail-recursive call.
The c code being weird is more of an implementation detail that shouldn’t be of much focus.
1. Find the thing to update
2. Update it
Linus's version does just that.
But the first version is more complex because step 1 instead emits something that may or may not be the thing to update, and so step 2 has to reason about that. Additionally, it introduces a bookkeeping variable -- cur -- which becomes redundant before step 2 (by which point it's equal to target).
IMO Linus is right here. The form of his solution directly matches the problem and allows you to look at the code as two clean steps -- no reasoning about the output of the first step and no bookkeeping cruft variable that you have to ignore or remind yourself that it's the same as another variable after some point in the function. At least once you're used to working with double pointers I think that's a much easier function to read.
There is a well-known saying attributed to David Wheeler, "All problems in computer science can be solved by another level of indirection." Except the problem of having too many layers of indirection. Also both quotes are often seeing with "abstraction" instead of "indirection".
There is a not well-known saying that you can attribute to me, "Any method of abstraction that you have internalized becomes conceptually free to you." (So for the sake of others, choose wisely which you will expect maintenance programmers to have internalized!)
The key to the elegant solution is understanding how to manipulate data through pointers.
That makes the elegant solution inappropriate to use in CS 101. It involves a method of indirection/abstraction that is emphatically NOT free to beginning programmers.
It also makes the elegant solution inappropriate for most people on HN. We do not directly deal with manipulating things through pointers very much. Therefore most of us have not internalized how to do that, and the technique is very much not free to us.
However Linus is a kernel developer. Anyone maintaining his code will also be a kernel developer. Kernel developers have to internalize how to handle manipulating data through pointers because their APIs require it. For example look at https://www.kernel.org/doc/html/v4.14/filesystems/index.html and see that pretty much every function gets a pointer to a data structure, and then manipulates that data structure through the pointer.
Therefore every kernel developer should internalize how to manipulate data through pointers. And the elegant solution therefore becomes not just less code, it becomes conceptually simpler! And yes, any time you can replace a block of code with less code that is conceptually simpler, this shows good taste.
BUT, and this is very important, it is only conceptually simpler if you've already internalized concepts around manipulating data through the indirection of a pointer. Which makes it conceptually simpler for kernel developers like Linus, but not for most programmers in other languages.
That's great because it kind of speaks to the crux of the problem.
But the first solution does use pointers :)
The second uses double pointers.
I'd argue that 'even kernel maintainers' may not be so easy with the second in reality.
It's probably worth it if there is a performance gain, because it's so low level. But not otherwise.
But good point.
What I do not understand is why one should use an "IntList" struct in the first place? As the explanation of the second method suggests, a List is the same thing as a pointer to its first element, so why not do this?:
typedef struct IntListItem* IntList;
Also, could it be that both methods fail terribly (infinite loops?) when they are given wrong input such as elements not in the list at all?
You could just say "has undefined behavior unless target is an element of the list".
Whatever your choice though - this is slide code. It should be obvious that it's not production ready.
This is pre-1990s programming style. I've seen such code in assembly programs. Because I was reading crash dumps where it failed.
(separate code exists for searching a list)
This... is not Python. C programmers, particularly kernel developers, have that style even in 2020.
This is a classic idea for doubly linked lists. The empty list has the head element linked to itself in both directions. Buffer rings are sometimes organized that way. The cases for doubly linked lists are messier.
Bear in mind that in modern CPUs, branches to nearby code are almost free, but indirection to far memory is expensive.
I've since discovered bsd/queue.h [0], which is very similar in purpose, but is not "good taste" (which I don't mind at all) on the other hand it is type safe, has quite a few variants for single and double lists, and oh also, it's not GPL.
[0]: https://github.com/freebsd/freebsd/blob/master/sys/sys/queue...
As a result, remove_element() is as simple as *(e->prev) = e->next;
void remove(IntList* l, IntListItem* target) {
if (l->head == target) {
l->head = l->head->next;
return;
}
IntListItem* prev = l->head;
while (prev->next != target) prev = prev->next;
prev->next = prev->next->next;
}
Skimming the comments here, I was surprised not to see an equivalent piece of code mentioned. To me my code is more readable than both of the first and second examples presented in the article. Does that mean my taste is peculiar?There are two cases here (1. when the target is at the front of the list and necessitates changing the head of list, and 2. when the head doesn't need to be changed.) You handle both the cases separately.
Both the classical and the "elegant" versions are worse than this one.
There is no "head case", all members of the list are the same. The head isn't a special element.
I'd say C syntax for double pointers is a lot less kind than the syntax for single pointers. Your version thankfully lacks any line like so, without making me think about parens
p = &(*p)->next;It is NOT better. It is much more difficult to understand. Software engineering is about making the code intelligible for the people who follow you. The simple two pointer with conditional is MUCH easier to read and understand.
To add context that seems lost on some: this is a cleaned-up version of my own notes from when I tried to understand the technical detail behind what Linus called "good taste" in the TED talk.
The main contributions of the writeup (if any) are the two conceptual insights that using an indirect pointer yields a homogeneous data structure and a convenient handle to the list item and its predecessor.
The article is not intended to show an example of clean code, there is no checking for NULLs, there are implicit expectations (the target needs to exist). It's just not the point.
I also strongly agree with the sentiment in the discussion that simple is often better than elegant. If it takes an entire article to figure out what's happening, that says something about how careful you should be with putting it in production code.
Anyways, thanks everyone on HN for a great discussion and for all the insights, comments and suggestions!
The elegant utility may be difficult to deal with standing alone, but when you see it 2,3,4 different places (by same author, or mimics) it becomes normalized, and once normal there’s really no reason not to use it.
The main problem with clever code is when it stands alone. Which is also the context in which these debates occur.
For the same reason, an empty sum is defined as 0 and an empty product as 1, so your base cases of some inductions don't require an extra "if".
Lastly my link object contained 5 pointers, so I XORed the object and box pointer to cut it down to 4. I always start traversal from one of those, so the XORed value can always be used to reach the other. This did not really impact performance much.
> it is not immediately evident how the more elegant solution actually works
another virtue is ease of comprehension, and the more elegant solution lacks that in my (and seemingly Linus') opinion. Maybe if you're used to working with pointers to pointers you might have an intuituon for them, but I at least had a difficult time gaining an intuitive grasp on the second solution, which could potentially nullify the bug-resistance of having fewer branching cases. In short, calling the second one objectively better is overstating it I think.
It's worth noting that in a language with union types, you can have the best of both worlds (using Rust here because it's the one I'm most familiar with):
enum LinkedList {
Null,
Node { value: i32, next: Box<LinkedList> }
}
In the same way that the pointer to a pointer homogenizes the head-case with the rest, a union type means that any given linked list "is just a node", and the head can therefore be treated the same way as any later node struct Node {
value: i32
next: Option<Box<Node>>
} struct List {
head: Option<Node>
}
struct Node {
value: i32,
next: Option<Box<Node>>
}I think rust probably has better tools for this.
Probably some sort of
let node = node.next;
while let Some(n) = node {
node = n.next
}I absolutely prefer the first one in almost all cases, and would probably reject the second one on a code review.
Unless we're dealing with such a core, hyper-sensitive part of the system wherein the compiler would not find rough equivalence anyhow, and the material gains from supposedly 'fewer instructions' would be better.
i.e. a pragmatic performance optimization that was realized in the real world, due to the pervasive utilization of the code ... this would be acceptable.
But for the vast majority of what we do, this won't be the case.
Double-pointers are like flame throwers - they are very 'cool' to some, and technically, they do 'burn things faster', but are just excessively dangerous and almost assuredly not the right too. Unless, they actually are, wherein you get to be the dude who uses the flamethrower, but again, that's rare.
Reading this it seems more clear to me why git has such tremendous - and mostly unnecessary UX problems. There's a dimensionality of the craft being ignored.
char *fn(char **foo, char ***bar){
char *baz = *++*foo ? *foo : *++*bar;
Double-pointers are just normal. The strtol function has one.Ok, it's possible the entirety of it makes sense, but given that C/C++ is full of absurd shenanigans, I think odds are something is wrong with a system that needs that kind of function in the first place.
That such things are common enough doesn't make them a good practice.
https://meta.slashdot.org/story/12/10/11/0030249/linus-torva...
Here using actually "pp" ;)
If you're someone who has experience with, or can easily grok concepts such as "pointer to a pointer", then the second snippet seems obviously simpler. After all, there are fewer branches to consider.
Unfortunately, as someone who stopped working with pointers a long time ago, my mind has to work in overdrive to understand any implementation that relies on a "pointer to a pointer". Hence why the first implementation is far easier for me to understand.
I'm sure we'll see many debates around which solution is simpler, but these debates will never reach a resolution. Depending on the skills and experiences you bring to the table, different people will objectively benefit from very different implementations.
https://software.rajivprab.com/2019/08/29/abstractions-are-i...
Many comments here are arguing that the first answer is actually better because it is clearer. I think it feels clearer to many of us not because it is actually simpler, but because it is the one we learnt at school / are used to see and therefore know by heart.
Linus is probably aware of this and may have meant to surprise the public with the second solution.
Why the second solution is better? Not because it does less branching and is more efficient. This misses the point. Not because there are fewer lines of codes and is terser. This also misses the point.
Fewer special cases means less ways to screw up, and also easier to follow. In the general case. Not only in this specific linked list example. And also clearer code.
It's just that in this specific case, we are used to the first solution that we are able to recognize at a glance (we "pattern-match" it).
Do you remember when you had to grasp this first solution the first time you encountered it or tried to write it? Many of us probably screwed it up and wasted time making it work. We might have forgotten the exact edge case Linus Torvalds was pointing out in this presentation. At least for me, I remember it was hard. I probably would have had easier time understanding the second solution by the way. The difficulty is a pointer indirection, but you better really understand pointers correctly when you are manipulating linked lists in C anyway.
Comments here also speak about leaving maintainable code for future developers on the project and avoid clever solutions to make their life easier, but it is the whole point of the second solution: let them not have to think about edge cases as much as possible.
Don't stop on this linked list example. We are all used to the first solution and Linus Torvalds probably picked this example because many people know linked lists. The message is: fewer edge cases is better. The goal is not to be "clever", in the negative sense.
Also see the original code with comments, which is way more readable: https://news.ycombinator.com/item?id=25327066
I think the idea of this extends much beyond a linked-list implementation, into software design and architecture.
Sometimes, you find more elegant solutions to something, that inherently do away with edge cases. I think this is the original intent, to show that you can find beauty, much as chessplayers do in chess. These solutions may be harder to understand completely, but you can actually encapsulate them in a function or use them as patterns!
A point is also made, there's often a rewrite involved. You usually don't need to find this stuff on first try.
Then I went back to Pascal, and designed a program in my head with some dynamically allocated linked list data structures, and another data structure that had a member that pointed to the head of the linked list.
Then I started typing in the Pascal code, and hit a wall, because Pascal has ^ which is like C's * operator to dereference a pointer, but doesn't have anything like C's & operator to make a pointer to an arbitrary field in memory, so you can't actually make a pointer to anything except the beginning of a record that you dynamically allocated!
That was when I gave up on Pascal.
Programming Pascal is like riding a bicycle with only one leg.
So Linus's elegant linked list solution is possible in C, but not Pascal.
I don't get your metaphor that Pascal is a motorcycle and C is a bicycle. Just the opposite.
Nowadays there is the @ operator
There was a trick for doing PEEK and POKE in Apple Pascal, using a union that contained a pointer to some type you could dereference to read and write, and also an integer so you could set the pointer. I had the hardest time understanding it, and just could not get my head around the "record case boolean of", but I just typed in the magic code with a bunch of weird ^'s and it worked somehow.
You can use type punning tricks with "union" to implement arbitrary unsafe pointer arithmetic in Pascal, since it lets you convert between pointers and integers.
https://en.wikipedia.org/wiki/Type_punning#Pascal
Here's an article about that trick, in German (which makes it sound even cooler):
https://www.robert-tolksdorf.de/book/Tolksdorf-UCSD-Pascal-C...
p. 63: 3.1 "PEEK" und "POKE" auch in Pascal
[...]
type byte = 0..255
spchrinhalt = packed array [0..0] of byte;
spchrstelle = record case boolean of
true: (adresse:integer); ( Zieger )
false: (inhalt:^spchrinhalt) ( Inhalt )
end;
procedure poke(adresse:integer; inhalt:byte);
var dummy:spchrstelle;
begin
dummy.adresse := adresse;
dummy.inhalt^[0] := inhalt;
end;
function peek(adresse:integer): byte;
var dummy:spchrstelle;
begin
dummy.adresse := adresse;
peek := dummy.inhalt^[0];
end;Not sure if this is actually a benefit or not. Edge cases are notoriously hard to debug, so it's sometimes actually nice to have a branch that specifically handles edge cases. Conceptually, it's also much more difficult to wrap one's head around. I would be interested to see how much of the cs101 solution is compiled away and if there are any tangible benefits of being clever here.
PS: If the linked list is stored in contiguous memory (if you're using a slab allocator, for example), you can actually be even more clever (I'll leave that as an exercise to the reader).
This might be a dumb question, but if list elements are stored contiguously, is there any advantage to using a linked list instead of a data structure that is designed for contiguous storage (something like C++'s std::vector)?
> if list elements are stored contiguously, is there any advantage to using a linked list
Storing them contiguously probably implies that you consider "freed" space in the middle to also be part of the contiguous area. Otherwise you can't remove in O(1) time.It is straightforward to maintain a list of freed nodes which you can add back later.
If you don't mind not being able to remove in O(1) time, you still have the advantage of passing the handi-capped (contiguous) linked list to interfaces that expect a linked list, but still get the cache locality of a plain vector.
times has changed. Back then the indirect was the standard approach (in particular you wouldn't want to waste registers). It requires just a bit more complicated reasoning, and the CS and the people in it were just a bit closer to math back then. Today it is basic engineering/craft, and thus the standard is the much simpler for reasoning approach with simple pointers and the simple explicit special case handling - ie our modern enterprise C code.
I would offer that there is no shame in doing something a bit more advanced, as long as there are test cases and documentation proportional to the advanced nature of the technique available.
EDIT: interestingly, I see a lot of comments here focusing on the reduction of lines of code (LOC). But IMO, the solution would still have been deemed "better code "if it had increase the LOC instead of reducing it. The idea is about eliminating edge-cases, not about reducing the number of LOC.
I believe that this approach has the advantage of being clearer.
Admittedly, one drawback of this approach is that it uses more space, which could be an issue in an application where you have many lists, nearly all empty.
https://news.ycombinator.com/item?id=18997420
It's interesting to see how divisive the opinions are. I see it as the difference between the "growth mindset" and not.
By using the pointer to the current element, you lose information (that element's ancestor), forcing you to introduce a "prev", and to track and update 2 variables.
By using a pointer to the pointer of the current element, you have access to all the information you need -- the "prev" and the "cur" -- just by following the pointer trail one or two steps.
item** find_parent(item** list, item* item) {
item** potential_parent = list;
while(*potential_parent != item) {
potential_parent = &(*potential_parent)->next;
}
return potential_parent;
}
void remove_item(item** list, item* item) {
item** parent = find_parent(list, item);
*parent = item->next;
}for(p=NULL,q=head; q!=entry;p=q,q=q->next); *(p?&p->next:&head) = q->next;
I respect the cleverness of Linus' solution. However, it really has no place in production code where not all journeymen are at the same lofty level.
The fact that it requires a detailed explanation exposes its impracticality.
https://www.ted.com/talks/linus_torvalds_the_mind_behind_lin...
Both are smart IMO.
Sometimes I improved my code a few days after I wrote them, because I found another interpretation of the original problem.
a) When your nodes can belong to multiple lists. You can't do this with arrays.
b) When you need fast removal from the middle of the list and you already have a pointer to the list node [2].
[1] In an intrusive linked list, the prev/next pointers are members of the payload node, as opposed to a "simple" linked list, in which the list nodes contain a pointer to the payload.
[2] This happens all the time in kernels. For example, you receive an interrupt and need to remove the corresponding task from the IDLE queue and append it to the RUNNING queue.
I assume the author of this article had spent too much time being confused by the ‘Windows Subsystem for Linux’ nomenclature.
Submitters: before putting Show HN on a title, please read the rules: https://news.ycombinator.com/showhn.html.
After reading for a minute I realized it's all about pointer and C specific stuff, I am not going to revisit that just for an article...
I compiled the presented code out curiosity on arm gcc 8.2 on godbolt.org with -O3 op and the results are:
(the original remove is even faster then the so called elegant or the elegant with inline)
remove_cs101:
ldr r2, [r0]
cmp r2, r1
bne .L3
b .L9
.L6:
mov r2, r3
.L3:
ldr r3, [r2, #4]
cmp r1, r3
bne .L6
ldr r3, [r1, #4]
str r3, [r2, #4]
bx lr
.L9:
ldr r3, [r2, #4]
str r3, [r0]
bx lr
remove_elegant:
ldr r2, [r0]
cmp r1, r2
bne .L12
b .L13
.L14:
mov r2, r3
.L12:
ldr r3, [r2, #4]
cmp r1, r3
bne .L14
add r0, r2, #4
.L13:
ldr r3, [r1, #4]
str r3, [r0]
bx lr
remove_elegant_with_inline:
ldr r2, [r0]
cmp r1, r2
cmpne r2, #0
bne .L17
b .L18
.L19:
mov r2, r3
.L17:
ldr r3, [r2, #4]
cmp r1, r3
cmpne r3, #0
bne .L19
add r0, r2, #4
.L18:
ldr r3, [r1, #4]
str r3, [r0]
bx lr
EDIT: Hmmm, so much downvote without any reply.Well if your code needs an article to be explained, rather than the plain solution and brings nothing, even 1 instruction slower, as the compiler can't make the same (faster) result as the plain solution, than your code is pointless. And stating that this is the elegant solution is IMO bad practice.
Second, I think you're missing the point by focusing on the code generated by the compiler. Clearly the two algorithms are equivalent, and a good compiler can generate equivalent code for them, up to an instruction more or less. The bigger concern here is about the source code: how can we express the algorithm in the most elegant (think: clear, concise, and correct) way? The exact generated bytecode is irrelevant, so long as the compiler is able to generate something reasonable, which is clearly the case here, as you've demonstrated.
The "elegant" solution tries to be smart, or optimize where there is no need at all. Yes the simple "elegant" solution is just 1 instruction slower, and longer only in the end, but the inline version is 1 instruction slower in the loop, which is stated more elegant in the article.
EDIT: Or you can get the same principle as the Occam's razor, which is KISS keep it simple, stupid, silly and straightforward
In fact, I am not sure they are equivalent (in general, without taking into account the context), and even if they were, proving this would be S.F. for a compiler today (2020). Even coming up with such an optimisation without hardcoding it does not seem plausible.
IntListItem -> IntListItem -> IntListItem
and then draw the position of the list head: IntList -> IntListItem -> IntListItem -> IntListItem
You can see that the if-branch in the cs101 answer comes because there is a ('virtual') element of the list (the IntList head) that is different from the other elements of the list.If we were to make them the same (C# code):
interface IListItem
{
IntListItem Next {get;set;}
}
class IntListItem : IListItem
{
int Value {get; set;}
IntListItem Next {get; set;}
}
class IntList : IListItem
{
IntListItem Head {get; set;}
IntListItem Next
{
get { return Head; }
set { Head = value; }
}
}
then our cs101 code simplifies itself, folding into a prettier algorithm: void remove_cs102(IntList l, IntListItem target)
{
IListItem p = (IListItem) l;
while (p.Next != target)
{
p = (IListItem) p.Next;
}
p.Next = p.Next.Next;
}
the use of indirect pointers was masking the real issue: some algorithms look better if you add a virtual head (or a virtual tail) to your linked list. Emphasis on look though -- they work almost the same.Not sure why you are being downvoted.
The downvotes probably come from my blatant disregard of the real-world performance in the search of what I considered the real take-away from the article.
Edit: Within 30 seconds this got downvoted by cowards with no response. Enough with lurker culture. Say something.
1. For people who have worked on open source - just look through their code, their commit and you'll see how they think and operate.
2. If 1 isn't applicable, give them homework, not a test. Give them a very simple but very well documented task. Something along the lines of an authentication system, with password reset which is time restricted, basic encryption and security and that's it. This can be easily achieved in just about any language in several hundred lines of code. But given the adequate amount of time to think it through and develop it, you'll see if they come up with clever solutions to simple problems or a pile of duct tape hacks.