The big-endian naming convention (source_index, target_index instead of index_source, index_target) is also interesting. It means related variables sort together lexicographically, which helps with grep and IDE autocomplete. Small thing but it adds up when you're reading unfamiliar code.
One thing I'd add: this convention is especially valuable during code review. When every variable that represents a byte quantity ends in _size and every item count ends in _count, a reviewer can spot dimensional mismatches almost mechanically without having to load the full algorithm into their head.
At that point I'd rather make them separate data types, and have the compiler spot mismatches actually-mechanically o.o
u32AllocSumSizeInBytes
f32AllocAvgSizeInKBytes
Its easy to code review the interaction between these - the valid code almost writes itself from the names - lots of operations will just obviously read as wrong in ways that they may not with i.e. num_allocs, total_alloc and avg_alloc, or num, total, avg.
I find it useful to use statically typed variable names. My mental syntax checker benefits from the extra type metadata being embedded at each usage, reducing mental indirections/my reliance on the larger context and enabling me to quickly detect many types of mistakes via more efficient local reasoning.
I would call it “English naming” [0], it’s just more readable to start with, in an anglophone environment.
[0] as opposed to “naming, English”, I suppose ;)
For that matter, many languages, especially "object-oriented" ones, treat heterogeneous containers as the default. They might not even offer native containers that can store everything inline in a single contiguous allocation, except perhaps for strings. In which case, "number of bytes" is itself ambiguous; are you including the indirected objects or not?
"Count" is also overloaded — it commonly means, and I normally only understand it to mean, the number of elements in a collection meeting some condition. Hence the `.count` method of Python sequences, as well as the jargon "population count" referring to the number of set bits in an integer. Today, Python's integers have both a `.bit_count` and a `.bit_length`, and it's obvious what both of them do; calling either `.bit_size` would be confusing in my mental framework, and a contradiction in terms in the OP's.
I would disagree that even C's `strlen` refers to byte size. C comes from a pre-Unicode world; the type is called `char` because that was naively considered sufficient at the time to represent a text character. (Unicode is still in that sense naive, but it at least allows for systems that are acutely aware of the distinction between "characters" and graphemes.) But notice: C's "strings" aren't proper objects; they're null-terminated sequences, i.e. their length is signaled in-band. So that metadata is also just part of the data, in a single allocation with no indirection; the "size" of a string could only reasonably be interpreted to include that null terminator. Yet the result of `strlen` excludes it! Further, if `strlen` is used on a string that was placed within some allocated buffer, it knows nothing about that buffer.
(Similarly, Rust `str::len` is properly named by this scheme. It gives the number of valid 1-byte-sized elements in a collection, not the byte size of the buffer they're stored within. It's still ambiguous in a sense, but that's because of the convention of using UTF-8 to create an abstraction of "character" elements of non-uniform size. This kind of ambiguity is properly resolved either with iterators, like the `Chars` iterator in Rust, or with views.)
Also consider: C has a `sizeof` operator, influencing Python's `.__sizeof__()` methods. That's because the concept of "size" equally makes sense for non-sequences; neither "count" nor "length" does. So of course "length" cannot mean what the author calls "size".
For many years (decades?) now, I've been using "index" for 0-based and "number" for 1-based as in "column index" for a C/Python style [ix] vs. "column number" for a shell/awk/etc. style $1 $2. Not sure this is the best terminology, but it is nice to have something consistent. E.g., "offset" for 0-based indices means "off" and even the letter "o" in some case becomes "the zero of some range". So, "offset" might be better than "index" for 0-based.
It was originally proposed as lengthof, but the results of the public poll and the ambiguity convinced the committee to choose countof, instead.
`countof` removes the verb possibility - but that means that a preference for `countof` over `lengthof` isn't necessarily a preference for `count` over `length`.
Just lining things up neatly helps spot bugs.
It’s the one thing I don’t like about strict formatters, I can no longer use spaces to line things up.
struct FileNode {
parent: NodeIndex<FileNode>,
content_header_offset: ByteOffset,
file_size: ByteCount,
}
Where `parent` can then only be used to index a container of `FileNode` values via the `std::ops::Index` trait.Strong typing of primitives also help prevent bugs like mixing up parameter ordering etc.
I once had a coworker like that, whose identifiers often stretched into the 30-50 characters range.You really don’t want that.
That said, I'm not sure how 1-based indexing will solve off-by-1 errors. They naturally come from the fencepost problem, i.e. the fact that sometimes we use indexes to indicate elements and sometimes to indicate boundaries between them. Mixing between them in our reasoning ultimately results in off-by-1 issues.
My opinion is that 1-based indexing really exacerbates off-by-one errors, besides requiring a more complex implementation in compilers, which is more bug-prone (with 1-based addressing, the compilers must create and use, in a manner transparent for the programmer, pointers that do not point to the intended object but towards an invalid location before the object, which must never be accessed through the pointer; this is why using 1-based addressing was easier in languages without pointers, like the original FORTRAN, but it would have been more difficult in languages that allow pointers, like C, the difficulty being in avoiding to expose the internal representation of pointers to the programmer).
Off-by-one errors are caused by mixing conventions for expressing indices and ranges.
If you always use a consistent convention, e.g. 0-based indexing together with half-open intervals, where the count of elements equals the difference between the interval bounds, there are no chances for ever making off-by-one errors.
"Is there any reason to not just switch to 0-based indexing if we could? Seems like 1-based indexing really exacerbates off-by-one errors without much benefit"
The problem is that humans make off-by-one errors and not that we're using the wrong indexing system.
We can't choose to switch to 1-based indexing - either we use 0-based everywhere, or a mixture of 0-based and 1-based. Given the prevalence of off-by-one errors, I think the most important thing is to be consistent.
To use the terminology from the article, with 0-based indexing, offset = index * node_size. If it was 1-based, you would have offset = (index - 1) * node_size + 1.
And it became a convention even for high level languages, because no matter what you prefer, inconsistency is even worse. An interesting case is Perl, which, in classic Perl fashion, lets you choose by setting the $[ variable. Most people, even Perl programmers consider it a terrible feature and 0-based indexing is used by default.
You'd just get a different set of off-by-one errors with 1-based indexing.
Offset is ordinarily just a difference of two indices. In a container I don't recall seeing it implicitly refer to byte offset.
Always good to qualify your identifiers with units IMO (or types that reflect units).
It doesn't mean "byte length", so much as "byte" happens to be the element type. Unicode is conventionally represented as UTF-8, so the container can't be directly indexed to yield a character.
Everything else looks disgustingly verbose once you get used to them.