I think this is part of a common misconception by C programmers (and other similar languages) that the language is some how a model of the processor it runs on. Especially among those who started out with C a long time ago. This has not been true for a couple of decades or more now. C employs some notion of an abstract machine it runs on. Which matches actual machines to a greater or lesser extent.
Handling += 1 as a special case (if really necessary at all) is easy for a compiler. A language does not need a special increment operator unless reducing complexity of the compiler itself to a minimum is a goal. Looking at Rust, I see a very complex machinery, where such an optimization (if not done anyway by later stages of generating assembler code) would not be a huge issue.
On the contrary, adding yet another syntax makes the language more complex for humans. Not everyone will start with the programming language C or C++, thus they may not know the ++ syntax. Introducing yet another operator to them can make the language more complex. It does save a character in the source, but not sure if that's really worth it.
Both the advantages and disadvantages are marginal, I would say.
I agree the removal of x++; is to increase safety and avoid confusion, though I wonder why can't you just increment it on its own and disallow people from assigning a variable while incrementing, for example:
let y = x++:
just disallow this but allow this:
x++;
let y = x;
That would make more sense!
Because x += 1; already exists, is just as short, clearer, and a lot more general.
Yes it already does exist but what is wrong with having x++; if it is not being assigned to a variable, its kinda like a shortcut and it wouldn't have any confusions as we are disallowing let y = x++;?
When would you use it?
In C we typically use ++ to increment pointers and to count how many times a for loop goes around. Such things are discouraged in Rust so there is no pressing need for a short cut.
To my mind adding 1 to a variable is a significant feature of an algorithm. Why diminish its stature and hide it away with a tiny syntax?
It's not much a shortcut: it would save you from writing only one character, "1". That's the tiniest possible code savings.
Seeing a "1" is a good thing to indicate that a value is incremented by 1. If you now want to change the code to increment by 2, you can easily change it to 2 using the same notation.
"++" is a more cryptic notation than "+= 1".
It would introduce another way of doing the same thing, reducing consistency of code.
It would require adding another two traits to std::ops.
User types that implement some sort of arithmetic would have two more traits to implement.
If you care what's going on under the hood you should prefer add 1 over inc.
That's the wrong question for a language change. All language additions "start with -100 points", because more is bad, all else being equal. The very minimum bar for a language addition is "is this useful enough to overcome the cost of people needing to learn it?".
Thus, for something that's already possible, another way to do it needs to have a combination of being substantially shorter/simpler and that thing being a common enough task to be worth doing. Thus all the feedback in this thread about "the existing way isn't materially longer" and "it's not that common that one needs to do this anyway".
For fun: if you do let x = 4..;, then x++ is x.next().unwrap().
To this point, ++x has a reason to exist in C++: it's C++'s version of Iterator::next.
Well, it's more complicated than that, of course. Because it's C++ I'm talking about. At an overly simplified level, an iterator is any object r such that ++r advances the iterator and *r evaluates to the current iterated value. This is the absolute minimum of the pointer API which is technically usable.
++ is a meaningful operation for C++ to have because increment is a useful operation to have access to in a generic template environment (i.e. for iterators) where adding an integer may not make sense (or could even be given a different semantic by misguided developers).
For Rust, we have semantic, nominal traits rather than purely working from symbols with conventional meaning. The direct analog for most usage is Iterator::next, more commonly used via for $pat in $expr $block syntax. We don't have the for (init; test; incr) form that C and C++ use, so the primary way ++x gets used just doesn't exist.
That doesn't mean we don't have a separate notion of increment, though; we do, via trait core::iter::Step. However, interestingly enough, the current form of it (as of 1.68.0 (2022-12-30), and originally authored by me[1]) doesn't have actual increment/decrement operations (although it did previously); it just has strided Step::forward(start, count) and Step::backward(start, count) which take a step usize. It also doesn't even take the "iterator" by &mut, but operates instead entirely by value/copy. This is because in its current formulation Step is purely about expressing the interface used by Range to iterate iNN/uNN/char in a way that LLVM can easily cut through the Iterator noise, and this happened to be what we found worked.
I don't see us re-adding direct increment/decrement operations to Step; there's already necessarily three "modes" exposed (checked overflow, arbitrary safe overflow, and caller-unsafe-guaranteed-no overflow), and inlining constants is perhaps the easiest thing any competent optimizing backend does, so there's minimal if any benefit to emitted code quality along with measurable code maintenance quality pessimations to introducing non-strided forms. Iterator covers the iteration case perfectly well, so Step doesn't need to.
-
Like with much of Open Source, for selfish reasons: I really wanted
Range<char>to be iterable, so I went and redid theSteptrait so the necessary implementation was valid; at that point in timeStepwas in need of some attention anyway, withreplace_oneandreplace_zeromethods going completely unused and semantically not really part ofStep. Once the new trait version was merged, it was just a matter of providing the impl. ↩︎
One of the goals of Rust is to prevent the programmer from writing compilable code that results in undefined behavior. The post/pre-increment operator introduces opportunities to create such code. If you're not sure that's true, check all the questions in stack overflow in the C are about why various expressions involving the operator produce unexpected results. ![]()
Thing is, if Rust had a ++ operator then some clever clogs would go and create a new "improved" Rust++ language. Better not go down that road.
Rust isn't good because it looks like assembly.
Rust is good because it doesn't look like assembly (but still efficient).
As a direct counter-example to the original post, RISC-V doesn't have dedicated increment and decrement instructions (can be encoded as addi dst, dst, 1 and addi dst, dst, -1, respectively). Does that mean that C and C++ now have to remove the increment and decrement operators, because both compile to RISC-V? No, there are better reasons to do that ![]()
Also, ++ and -- do not necessarily increment a value by 1: pointers get incremented by sizeof(pointee):
#include <stdio.h>
int main()
{
int x = 0;
int *p = &x;
printf("p: %p\n", (void*) p);
printf("++p: %p\n", (void*) ++p);
}
prints
p: 0x7fff65f0855c
++p: 0x7fff65f08560
Fun fact: RISC-V doesn't have "move" and "nop" instructions either, which are encoded, again, with addi:
-
mv dst, src→addi dst, src, 0 -
nop→addi x0, x0, 0
What all of that means is that mimicking hardware in language design is pointless. There are many different architectures, each with their own quirks. The reason we have programming languages in the first place is to abstract away from the hardware.
It's important to note some history. My understanding is that the increment operator in C was meant to support an addressing mode on the PDP-11 that did operations like *x++ = y in a single instruction. The increment or decrement operators themselves are not that useful but in combination with dereferencing they enable very succinct code. However they lend themselves to code that is confusing at best and paradoxical at worst (what does *++x = *x do?). Personally I don't mind not having them. I've never seen a style guide for C or C++ that recommended using the dereference and increment idiom.
While I generally agree with the conclusion of your answer, I'd like to note that in C parlance, incrementing a pointer by 1 unit is incrementing it by 1 unit. We don't say that the pointer incremented by 4 or 8 or however many bytes sizeof(T) is; the generally accepted terminology is that pointer arithmetic is implicitly understood in terms of T units, and specific integer values extracted for debugging purposes are irrelevant when discussing the operational semantics of the C abstract machine.
People often guess that they were created to use the auto-increment and auto-decrement address modes provided by the DEC PDP-11 on which C and Unix first became popular. This is historically impossible, since there was no PDP-11 when B was developed. The PDP-7, however, did have a few `auto-increment' memory cells, with the property that an indirect memory reference through them incremented the cell. This feature probably suggested such operators to Thompson; the generalization to make them both prefix and postfix was his own. Indeed, the auto-increment cells were not used directly in implementation of the operators, and a stronger motivation for the innovation was probably his observation that the translation of ++x was smaller than that of x=x+1.
This shows a few things:
- 1960s compilers didn't optimize. The rationale of the last statement only makes sense in that light.
- Language choices based on special hardware features become outdated quickly because hardware changes faster than programming language features.
Regarding the last point, I think Rust has made a few similar mistakes (1u32 << 32 doesn't work, and -3 % 10 returns -3 rather than 7, both are usability failures motivated by contemporary hardware details).
I had to read this line twice and in context. At first I thought it was the strangest criticism of Rust I'd ever seen.
Thanks for the correction!
Rust did that mistake with conflating usize as both intptr_t and size_t, but I think it did the right call with << and %.
C developers were certainly burned by that, then C/C++ committees took that idea seriously… and C/C++ developers were burned again.
Take that idea about the fact that signed overflow is UB (needed to support one's complement machines, which not even Unisys makes today) or type based memory access rules (extremely important if your coprocessor is separate piece of hardware like Weitek 4167 or just good old 8087)… the fact that these were not optional extensions/relaxations, but mandatory parts of C++ caused untold grief.
Just what CPU you have in mind that would work differently?
ARM used to be an example of weird architecture where 1u32 << 32 worked. Today… AArch64… here we go:
result = ShiftReg(n, shift_type, UInt(operand2) MOD datasize, datasize);
RISC-V? Sure: SLL, SRL, and SRA perform logical left, logical right, and arithmetic right shifts on the value in register rs1 by the shift amount held in the lower 5 bits of register rs2 (for rv32i) and SLL, SRL, and SRA perform logical left, logical right, and arithmetic right shifts on the value in register rs1 by the shift amount held in register rs2. In RV64I, only the low 6 bits of rs2 are considered for the shift amount (rv64).
Hardware used to be changing faster than programming language features. Not anymore.
Today only highly-specialized hardware can afford to do that (like GPUs) — but they can afford that precisely when large pile of existing software written in existing programming languages is not supposed to be used… that, of course, makes it highly unlikely that Rust would be used there a lot, too.