Date: Thu, 30 Mar 2023 16:28:18 +0200
On 30/03/2023 11:26, Alejandro Colomar via Std-Proposals wrote:
>
>
> On 3/30/23 10:58, David Brown via Std-Proposals wrote:
>> On 29/03/2023 17:18, Arthur O'Dwyer via Std-Proposals wrote:
>>
>>> A new `uintmax_extended_t` (or whatever) can communicate properly from
>>> the get-go: "Hey! This type will change in the future! Don't build it
>>> into your APIs!"
>>> But then, if you aren't using this type in APIs, then where /*are*/ you
>>> using it, and why does it need to exist in the standard library at all?
>>>
>> That, I think, is the key point - /why/ would you want a "maximum size
>> integer type" ?
>
> For example to printf(3) an off_t variable.
> Or to write a [[gnu::always_inline]] function that accepts any integer.
> Or to write a type-generic macro that handles any integer.
>
That's a C problem, not a C++ problem. (Of course keeping compatibility
between the languages is a good thing, if it does not bring
disadvantages to one of the languages.)
> The addition of functions that handle [u]intmax_t was the design mistake.
> If they had been added as macros, we wouldn't be discussing ABI issues,
How could there have been the type intmax_t that could be used in
macros, but not as a parameter or return type for a function?
The smart thing (it's always easier with hindsight!) to have done in C99
was to move over to the size-specific types. "Long long" should never
have existed, nor the "[u]intmax_t" types. Standard library functions
and macros that had variants for different integer sizes (like abs)
should have switched to abs32, abs64, etc., with the old names kept for
compatibility (it doesn't matter if some of these are just macro names
for other variants). Type-generic versions could have been made with
compiler magic in C99, and _Generic in C11, using preprocessor checks
for the existence of sizes 32, 64, 128, etc. It would not have been
perfect (the stopping point for such _Generics would be arbitrary), and
there are plenty of other complications to consider, such as literal values.
> because macros don't have ABI. Of course, the problem is that _Generic(3)
> was only added in C11, but intmax_t was added in C99, so they had to do it
> as functions. History sucks.
>
One of biggest advantages of C and C++ is also one of their biggest
disadvantages - backwards compatibility.
> Still, I think the reform that has been done to make intmax_t be the
> widest standard integral type (or however they call it technically) (as
> opposed to the widest integral type including extended ones) is that it
> should be optimal for printing most libc variables of weird types such as
> id_t or off_t. And that can have a fixed ABI, because anyway, I don't
> expect libc to start using int128 for anything. Then if we get a new
> intwidest_t that covers all extended integer types (and maybe or maybe
> not the bit-precise ones) that one would be useful for other things, but
> that one would have a strong requirement of not being usable in anything
> that sets an ABI.
>
>>
>> I hope that intmax_t gets deprecated, and I hope never to see any
>> replacement versions in the C or C++ standards, regardless of how
>> clearly they are defined. The size of an integer that you want in your
>> code is determined by what you want to store in it - not the biggest
>> size some implementation happens to support.
>>
>> In my eyes, the appropriate starting point for C++ integer types would
>> be templates:
>>
>> Int_t<N>
>> Int_fast_t<N>
>> Int_least_t<N>
>
> What's the use of int_least_t? Now that C23 will require 2's complement
> and 8-bit char, it's hard to think of implementation that don't have
> the fixed-width types.
Fair point. But once you get to bigger sizes, implementations would not
be restrained to bit sizes that are powers of two - merely multiples of
8. And "N" does not have to be a number matching a specific real size.
Int_fast_t<40> could be 64-bit, with Int_least_t<40> being 5 bytes and
Int_t<40> a compile-time error. (I've used targets where there are
20-bit and 40-bit types - and the motivation for _BitInt() is
programmable logic where bit sizes for variables can be arbitrary, even
if storage is in lumps of 8-bit bytes.)
>
> Even the int_fast_t ones are already dubious, since fastness depends on
> context. Usually if you don't care about the width of your ints, you can
> just use int or long and you'll be fine. Why would you type int_fast16_t
> over int? I bet you're not going to notice that int is not always the
> size of a word.
>
Some code has to be portable. int_fast16_t is 64-bit on x86-64, but
16-bit on an AVR and 32-bit on 32-bit ARM. I agree that "fastest size"
is not always well-defined, but it can be helpful.
It is certainly the case, IME, that the [u]intN_t types in <stdint.h> /
<cstdint> are by far the most popular - but the others are used sometimes.
>>
>> matching the standard int32_t, int_fast32_t, int_least32_t types (and
>> all similar types). They should support N as powers of 2 from 8
>> upwards, but implementations could support other sizes if they want (the
>> "fast" and "least" types will always support other sizes, rounding up as
>> needed).
>>
>> (I'd also like variants with different overflow behaviours, but that's
>> an orthogonal issue.)
>>
>>
>> That would give a simple and consistent system that lets you have types
>> the size you want, and use them safely in API's.
>
> Nothing is really safe because of default int promotions. That's the
> original sin. Especially when you want 16-bit bitwise operations.
The integer promotions are value-preserving. There are very few
circumstances where they cause trouble - 16-bit bitwise operations
operate fine AFAIK, at least when there is a sane conversion to smaller
signed integer sizes. (I know of no implementation that uses anything
but truncation for that.) The only case I know about where integer
promotions can cause trouble is if you have a system with 32-bit int,
and multiply two uint16_t values to get a result greater than INT_MAX,
such as 60000 * 60000. This would be defined behaviour in pure 16-bit
unsigned arithmetic, but undefined behaviour in 32-bit signed arithmetic.
(There are also the "usual arithmetic conversions" that can change the
signedness of a value when you mix signed and unsigned types - they are
"original sin" IMHO.)
> _BitInt() is somehow better than the fundamental types, since it doesn't
> default to int. But you can't typedef it.
It is designed for quite specific and niche purposes, rather than for
general-purpose integers of different sizes.
>
>> The same type would
>> have the same size whether you are using an 8-bit AVR or a 128-bit
>> RISC-V (if anyone actually makes one).
>>
>> Things are more difficult in C, since there are no templates (_Generics
>> are not extensible or recursive, and cannot be parametrised by
>> integers). But again, the maximum size integer types were a mistake IMHO.
>>
>>
>
>
> On 3/30/23 10:58, David Brown via Std-Proposals wrote:
>> On 29/03/2023 17:18, Arthur O'Dwyer via Std-Proposals wrote:
>>
>>> A new `uintmax_extended_t` (or whatever) can communicate properly from
>>> the get-go: "Hey! This type will change in the future! Don't build it
>>> into your APIs!"
>>> But then, if you aren't using this type in APIs, then where /*are*/ you
>>> using it, and why does it need to exist in the standard library at all?
>>>
>> That, I think, is the key point - /why/ would you want a "maximum size
>> integer type" ?
>
> For example to printf(3) an off_t variable.
> Or to write a [[gnu::always_inline]] function that accepts any integer.
> Or to write a type-generic macro that handles any integer.
>
That's a C problem, not a C++ problem. (Of course keeping compatibility
between the languages is a good thing, if it does not bring
disadvantages to one of the languages.)
> The addition of functions that handle [u]intmax_t was the design mistake.
> If they had been added as macros, we wouldn't be discussing ABI issues,
How could there have been the type intmax_t that could be used in
macros, but not as a parameter or return type for a function?
The smart thing (it's always easier with hindsight!) to have done in C99
was to move over to the size-specific types. "Long long" should never
have existed, nor the "[u]intmax_t" types. Standard library functions
and macros that had variants for different integer sizes (like abs)
should have switched to abs32, abs64, etc., with the old names kept for
compatibility (it doesn't matter if some of these are just macro names
for other variants). Type-generic versions could have been made with
compiler magic in C99, and _Generic in C11, using preprocessor checks
for the existence of sizes 32, 64, 128, etc. It would not have been
perfect (the stopping point for such _Generics would be arbitrary), and
there are plenty of other complications to consider, such as literal values.
> because macros don't have ABI. Of course, the problem is that _Generic(3)
> was only added in C11, but intmax_t was added in C99, so they had to do it
> as functions. History sucks.
>
One of biggest advantages of C and C++ is also one of their biggest
disadvantages - backwards compatibility.
> Still, I think the reform that has been done to make intmax_t be the
> widest standard integral type (or however they call it technically) (as
> opposed to the widest integral type including extended ones) is that it
> should be optimal for printing most libc variables of weird types such as
> id_t or off_t. And that can have a fixed ABI, because anyway, I don't
> expect libc to start using int128 for anything. Then if we get a new
> intwidest_t that covers all extended integer types (and maybe or maybe
> not the bit-precise ones) that one would be useful for other things, but
> that one would have a strong requirement of not being usable in anything
> that sets an ABI.
>
>>
>> I hope that intmax_t gets deprecated, and I hope never to see any
>> replacement versions in the C or C++ standards, regardless of how
>> clearly they are defined. The size of an integer that you want in your
>> code is determined by what you want to store in it - not the biggest
>> size some implementation happens to support.
>>
>> In my eyes, the appropriate starting point for C++ integer types would
>> be templates:
>>
>> Int_t<N>
>> Int_fast_t<N>
>> Int_least_t<N>
>
> What's the use of int_least_t? Now that C23 will require 2's complement
> and 8-bit char, it's hard to think of implementation that don't have
> the fixed-width types.
Fair point. But once you get to bigger sizes, implementations would not
be restrained to bit sizes that are powers of two - merely multiples of
8. And "N" does not have to be a number matching a specific real size.
Int_fast_t<40> could be 64-bit, with Int_least_t<40> being 5 bytes and
Int_t<40> a compile-time error. (I've used targets where there are
20-bit and 40-bit types - and the motivation for _BitInt() is
programmable logic where bit sizes for variables can be arbitrary, even
if storage is in lumps of 8-bit bytes.)
>
> Even the int_fast_t ones are already dubious, since fastness depends on
> context. Usually if you don't care about the width of your ints, you can
> just use int or long and you'll be fine. Why would you type int_fast16_t
> over int? I bet you're not going to notice that int is not always the
> size of a word.
>
Some code has to be portable. int_fast16_t is 64-bit on x86-64, but
16-bit on an AVR and 32-bit on 32-bit ARM. I agree that "fastest size"
is not always well-defined, but it can be helpful.
It is certainly the case, IME, that the [u]intN_t types in <stdint.h> /
<cstdint> are by far the most popular - but the others are used sometimes.
>>
>> matching the standard int32_t, int_fast32_t, int_least32_t types (and
>> all similar types). They should support N as powers of 2 from 8
>> upwards, but implementations could support other sizes if they want (the
>> "fast" and "least" types will always support other sizes, rounding up as
>> needed).
>>
>> (I'd also like variants with different overflow behaviours, but that's
>> an orthogonal issue.)
>>
>>
>> That would give a simple and consistent system that lets you have types
>> the size you want, and use them safely in API's.
>
> Nothing is really safe because of default int promotions. That's the
> original sin. Especially when you want 16-bit bitwise operations.
The integer promotions are value-preserving. There are very few
circumstances where they cause trouble - 16-bit bitwise operations
operate fine AFAIK, at least when there is a sane conversion to smaller
signed integer sizes. (I know of no implementation that uses anything
but truncation for that.) The only case I know about where integer
promotions can cause trouble is if you have a system with 32-bit int,
and multiply two uint16_t values to get a result greater than INT_MAX,
such as 60000 * 60000. This would be defined behaviour in pure 16-bit
unsigned arithmetic, but undefined behaviour in 32-bit signed arithmetic.
(There are also the "usual arithmetic conversions" that can change the
signedness of a value when you mix signed and unsigned types - they are
"original sin" IMHO.)
> _BitInt() is somehow better than the fundamental types, since it doesn't
> default to int. But you can't typedef it.
It is designed for quite specific and niche purposes, rather than for
general-purpose integers of different sizes.
>
>> The same type would
>> have the same size whether you are using an 8-bit AVR or a 128-bit
>> RISC-V (if anyone actually makes one).
>>
>> Things are more difficult in C, since there are no templates (_Generics
>> are not extensible or recursive, and cannot be parametrised by
>> integers). But again, the maximum size integer types were a mistake IMHO.
>>
>>
Received on 2023-03-30 14:28:28