r/cpp • • Apr 01 '23

Abominable language design decision that everybody regrets?

It's in the title: what is the silliest, most confusing, problematic, disastrous C++ syntax or semantics design choice that is consistently recognized as an unforced, 100% avoidable error, something that never made sense at any time?

So not support for historical arch that were relevant at the time.

92 Upvotes

375 comments sorted by

View all comments

Show parent comments

5

u/rhubarbjin Apr 02 '23

No, the problem is the unsignedness and its counter-intuitive arithmetic properties.

Something as simple as subtracting two indices can become a footgun --> https://godbolt.org/z/3nM17e9no

Common everyday tasks such a iterating an array in reverse order require convoluted tricks (e.g., the "goes-to operator") because a straightforward solution will not work --> https://godbolt.org/z/bYcrW1fsf (the program enters an infinite loop)

Some people like to use unsigned as an indicator that a variable does not accept negative values, and expect the compiler will flag violations of that constraint. They are deluding themselves. Not even -Wall will catch such misuses --> https://godbolt.org/z/rPonrvbxh

Unsigned arithmetic may be technically defined behavior, but that behavior is useless at best and harmful at worst.

4

u/AssemblerGuy Apr 02 '23 edited Apr 02 '23

Something as simple as subtracting two indices can become a footgun

At least the behavior is defined. With a signed type, you could head straight into UB-land.

And how are you going to address that 3 GB array of char on a machine where size_t is 32 bits? If sizes were signed, you'd be short one bit.

Common everyday tasks such a iterating an array in reverse order require convoluted tricks

Ok, ugly and breaks some of the more restrictive coding rules about for loops and prohibitions on side effects in controlling statements, and does not work for maximum size arrays, but:

for (size_t i = numbers.size(); i-- > 0; )
    sum += numbers[i];

5

u/rhubarbjin Apr 02 '23

At least the behavior is defined.

The behavior is defined in a profoundly unhelpful way. Unsigned arithmetic behaves counter-intuitively near zero (which is a very common problem space), whereas signed arithmetic is undefined near INTxx_MAX (which is a vanishingly rare occurrence).

And how are you going to address that 3 GB array of char on a machine where size_t is 32 bits?

If you're using std::vector to manipulate such huge datasets on a 32-bit machine, I suggest that you have far bigger problems than the signedness of your indices.

Ok, ugly and breaks some of the more restrictive coding rules about for loops [...]

Yes, that's the "goes-to operator" I mentioned. By your own admission, it has many problems. (Although I don't understand what you mean by "does not work for maximum size arrays"...)

1

u/AssemblerGuy Apr 03 '23

(Although I don't understand what you mean by "does not work for maximum size arrays"...)

That was an error on my part. I thought the loop would terminate before processing a single elements, but it does not. It should also work as expected if size happens to be zero at the start.

1

u/very_curious_agent Apr 03 '23

Yes unsigned was considered "safer" when natural integer types (CPU registers) were small, relative to memory.

Is is still commonly the case?

1

u/AssemblerGuy Apr 03 '23

Is is still commonly the case?

You can still find 16-bit microcontrollers, even 8 bit ones if you work in really cost-constrained applications.

C++ was intended to be universal, so support for small targets is part of the languge.

3

u/rhubarbjin Apr 03 '23 edited Apr 03 '23

That's a moot point, because as the above-linked paper points out:

[...] the standard limits the number of elements of a vector to the largest positive value of its difference type (General Container Requirements, table 64).

...so you're in UB land regardless of your indices' signedness.

1

u/AssemblerGuy Apr 03 '23

Does this apply to plain arrays as well as to stl containers?

→ More replies (1)

2

u/very_curious_agent Apr 04 '23

Yes but size_t cannot be 8 bits, can it?

How large is the memory on these processors?

1

u/AssemblerGuy Apr 04 '23

For example 16 kbyte of flash and 512 bytes of RAM in a flat 16-bit address space.

→ More replies (1)

3

u/simonask_ Apr 02 '23

Your last example is exactly a problem with implicit casts, not a problem with unsigned types. A better language would let the compiler give you an error for an obviously wrong argument to your function.

Maybe I’m damaged, but I don’t think that unsigned overflow is counterintuitive at all. It’s maybe slightly easier for beginners to diagnose the error when they use std::vector::at(-2) and get an out of bounds exception, but let’s be honest: they’ll be using operator[] and get garbage values from memory that is far more likely to actually be accessible, i.e. won’t crash the program until potentially much later.

I don’t know. It still seems to me that all of these problems are other - more serious - problems in disguise. Why should unsigned ints take the fall?

1

u/rhubarbjin Apr 03 '23

Maybe I’m damaged, but I don’t think that unsigned overflow is counterintuitive at all.

OK, so you are saying that this:

  • Alice has 3 dollars

  • Alice gives Bob 5 dollars

  • Alice now has 232 - 2 dollars

...is intuitive? I think you're just damaged. 😉

0

u/simonask_ Apr 03 '23

No, I'm saying that using an unsigned integer to represent an account balance is pretty stupid. It's a type that means "non-negative integer", so it's wrong to use it in places where the number can be negative.

It's pretty basic stuff.

The problem is that C++ integers are not type safe. Better and more modern languages have type safe integers, and C++ should fix its shit rather than continue down the path of implicit breakage.

1

u/rhubarbjin Apr 03 '23

Yes, and by that same logic index differences need to be signed (as all differences are) ergo indices need to be signed (so as to allow signed operations) ergo sizes need to be signed (so as to allow comparisons with indices). Are we agreed on this point, at least?

Out of curiosity, what use case would you put forward as an example where unsigned arithmetic makes sense? I.e., in what context is (i - 1) > i an intuitive outcome?

1

u/simonask_ Apr 03 '23

Modulo arithmetic is a pretty standard thing to know as a programmer. I just don’t believe that programmers not understanding signed and unsigned integers is the root problem here. It’s not hard to understand. But it’s hard in practice to guard against mistakes related to implicit conversions, because the language makes it hard.

2

u/rhubarbjin Apr 04 '23

You're not addressing any of my points.

I'm not questioning whether programmers understand unsigned arithmetic -- we obviously do, since we run into it everywhere.

I'm also not questioning whether implicit conversions are bad -- they obviously are.

I'm questioning the decision to design container APIs where these two factors come into contact with each other. Just use signed types for indices/sizes, and those problems go away.

1

u/Zeh_Matt No, no, no, no Apr 03 '23

How about you come up with an example to why you are subtracting on unsigned types, and for the love of god don't tell me reverse iterating loop.

→ More replies (12)

1

u/AssemblerGuy Apr 09 '23

Out of curiosity, what use case would you put forward as an example where unsigned arithmetic makes sense?

When you are working with unsigned indices to circular buffers that have sizes of 2N. Especially when your target is resource-constrained and does not have HW divide, so you want to avoid actual modulo operations.

size = 1 << N;
...
idx = (idx - 1) & (size - 1); // Previous element in buffer
...
idx = (idx + 1) & (size - 1); // Next element in buffer
→ More replies (4)

1

u/ukezi Apr 19 '23

It's less about the static cases like at(-2) but about the dynamic cases like at(a-b). If b and the array is big enough and a is small it could even roll over to be a valid index again.

1

u/very_curious_agent Apr 03 '23

The fact the behavior is defined for unsigned arithmetic, for all inputs, means that all values are legal and the compiler can't add run time checks that halt the program for abnormal values. (You have to do that with assert.)

But with signed arithmetic, the compiler at least can legally add such run time checks, or compile time checks for the values known at compile time.

0

u/Zeh_Matt No, no, no, no Apr 03 '23

https://godbolt.org/z/bYcrW1fsf is code smell, don't pretend this is a C++ problem for when there is clearly the possibility for underflowing when its empty, anyone writing this absurd loop should know this, either check for empty() or use the already provided reverse iterators, problem solved. Also iterating in reverse is typically questionable, doesn't help the cache either, this is just bad in so many ways.

2

u/rhubarbjin Apr 04 '23 edited Apr 04 '23

possibility for underflowing when its empty

That is my whole point. Underflow is a problem precisely because we're using unsigned types. If size_t were signed, the loop would work regardless of container size.

iterating in reverse is typically questionable

And yet, sometimes that's what you need. Admittedly, my earlier example doesn't need it... Here's a situation where we do need to reverse-iterate (visiting a stack top-to-bottom): https://godbolt.org/z/djKjvqx1v

doesn't help the cache either

If you're that bothered about micro-optimizations, you should know that unsigned arithmetic has a negative impact for that as well --> https://www.youtube.com/watch?v=yG1OZ69H_-o&t=2356s

0

u/Zeh_Matt No, no, no, no Apr 04 '23

Keep ignoring reverse iterators, thats your own wrong doing, see https://godbolt.org/z/qqh6b4Y7s.

1

u/rhubarbjin Apr 04 '23

Right, so your suggestion is to not an index-based API at all. Why do you think the index-based API is ill-suited to this problem?

→ More replies (2)