Exploring Variable Integer Widths in C23

As C evolved, so did the definitions for integer sizes short, int, and long. Back when I first coded C, a short was 8-bits, an int was 16-bits, and a long was 32-bits. These values aren’t the same today or even consistent across platforms. This issue is addressed in the C23 standard by using the _BitInt() data type.

Some compilers have implemented work-arounds for the inconsistency with bits in an integer data type. For example, the stdint.h header file defines these data types, which I’ve written about before:

int8_t is an 8-bit integer.
int16_t is a 16-bit integer.
int32_t is a 32-bit integer.
int64_t is a 64-bit integer.

You can employ these data types to ensure bit widths are consistent, but the _BitInt() data type in C23 makes this consistency part of the language. Plus it adds flexibility to let you set nonstandard bit width values, such as 12 or 24.

Unlike traditional C language data types, the _BitInt(n) type features an argument, n, which sets the bit width. For example, _BitInt(16) declares a 16-bit integer value. This value is consistently 16-bits regardless of the size of a short, int, or however else you could declare the variable.

The value of n in _BitInt(n) must be declared as a constant or literal. Yes, I tried to use a variable and the compiler (clang-15) was smart enough to flag my attempt as an error.

The maximum bit size you can declare when using the _BitInt() type is set by the BITINT_MAXWIDTH constant, defined in the limits.h header file.

Like other integer data types, you can create a signed or unsigned _BitInt() value. The number of bits you specify always includes the sign bit, for unsigned values.

The following code declares an 8-bit integer and outputs its value. The code also includes the limits.h header file to output the maximum bit width allowed for a _BitWidth() variable.

2024_01_06-Lesson.c

#include <stdio.h>
#include <limits.h>

int main()
{
    _BitInt(8) supershort;

    supershort = 22;

    printf("The 8-bit variable has a value of %d\n",(int)supershort);
    printf("_BitInt(%d) uses %lu bytes in memory\n",
            BITINT_MAXWIDTH,
            sizeof(_BitInt(BITINT_MAXWIDTH))
          );

    return 0;
}

For the first printf() statement, I typecast the supershort variable to an int. The program builds without the typecast, though it throws a warning as the compiler doesn’t accept that the %d placeholder matches a _BitInt() data type — and it’s correct. I don’t know of any placeholder specific to this new data type, so I typecast the variable to suppress the warning.

Remember that the code must be built with the -std=C2x switch and with a compiler compatible with the C23 standard. Here’s the output:

The 8-bit variable has a value of 22
_BitInt(128) uses 16 bytes in memory

The first line doesn’t really confirm that the variable uses only 8-bits. But the second line shows the maximum value available for the _BitInt() data type on my machine.

I tried creating super short integers — and it worked! The declaration _BitInt(4) creates a 4-bit signed integer value storing numbers in the range -8 through 7. My inner nerd is thrilled.

The C23 standard also employs specific bit-width data types for real numbers:

_Decimal32
_Decimal64
_Decimal128

These data types are compliant with various IEEE standards. This addition is necessary as like integer data types, float, double, and long double may use different bit widths on different systems. These new data types ensure consistency across all platforms.

In next week’s Lesson I cover a new method of initializing an array, one that will finally be consistent across all C23 compatible compilers.

6 thoughts on “Exploring Variable Integer Widths in C23

  1. While I was aware of plans to add “bit-precise integer types” to the language, I havenʼt as of yet had the chance to play around with them.

    That being said, I donʼt think anyone really will replace typedefs of the form “typedef int int32_t;” with, for example, “typedef _BitInt(32) int32_t;”. The main reason being, that—as the standard notes—“Each value of N designates a distinct type.”, _BitInt(N)ʼs thus being exempt from the integer promotion rules.

    What caught my eye, however, is that the release notes for Clang 16.0.0 contain the following interesting tidbit: “Lift _BitInt() supported max width from 128 to 8388608”.

    This suddenly makes BitInt()ʼs interesting, as this could, for example, allow developers of cryptography libraries to (possibly) replace their custom “bigint” types (e.g. OpenSSLʼs BN implementation) with these “bit-precise integer types”.

    While writing this, Iʼm beginning to have serious doubts about even this possibility, however—I guess only future will tell, if this really finds its use case(s) or becomes as frequently used as the ergometer in my living room…

  2. One other note: sorry to be that guy, but the paragraph about the new _Decimal types is factually incorrect.

    The new _Decimal types were not added because float, double, and long double may use different bit widths on different systems (binary floating-point representations are mandated by IEEE 754-1985, thus theyʼre the same across languages and platforms), but rather to bring decimal floating-point numbers (as first standardized in IEEE 754-2008) to “C”.

    Those types thus finally being able to represent decimal floating-point values faithfully (float and double only being able to represent values accurately, if they can be represented as sum of powers of two, e.g. 0.3125 = 2⁻² + 2⁻⁴).

    With _Decimal32, _Decimal64 and _Decimal128 one will, among other use cases, finally be able to do monetary calculations without having to fear those dreaded rounding errors:

    #define __STDC_WANT_DEC_FP__

    #include <stdio.h>

    static void test_floating_point (void);

    int main (void)
    {
      test_floating_point ();
      return (0);
    }

    static void test_floating_point (void)
    {
    /* Binary floating point values, e.g. 1001.100001110101 */

      /* 32-bit “single” binary floats: ±2¹²⁶ … ±(2-2²³)x2¹²⁷
         with FLT_DIG equal to 6 and a FLT_EPSILON of 1E-5 */
      float f32 = 0.1f;

      /* 64-bit “double” binary floats: ±2¹⁰²² … ±(2-2⁵²)x2¹⁰²³
         with DBL_DIG equal to 15 and a DBL_EPSILON of 1E-9 */
      double f64 = 0.1;

    /* Decimal floating point values, e.g. 3.1415926 */

      /* 32-bit decimal floats: ±0.000000×10⁻⁹⁵ … ±9.999999×10⁻⁹⁶
         with DEC32_MANT_DIG equal to 7 and a DEC32_EPSILON 1E-6df */
      _Decimal32 df32 = 0.1df;  

      /* 64-bit decimal floats: ±0.000000000000000×10⁻³⁸³ … ±9.999999999999999×10³⁸⁴
         with DEC64_MANT_DIG equal to 16 and a DEC64_EPSILON of 1E-15dd */
      _Decimal64 df64 = 0.1dd;  

      /* Add some small decimal value: */
      f32 += 0.2f;
      f64 += 0.2;

      df32 += 0.2df;
      df64 += 0.2dd;

      /* … and print the result: */
      fprintf (stdout, "1.0f / 3.0f == %.8f\n", f32);
      fprintf (stdout, "1.0 / 3.0 == %.17lf\n", f64);

      fprintf (stdout, "1.0df / 3.df == %.7Hf\n", df32);
      fprintf (stdout, "1.0dd / 3.dd == %.16Df\n", df64);
    }

    user@debian:~$ gcc -std=c2x -o decfltp decfltp.c
    user@debian:~$ ./decfltp
    1.0f  / 3.0f == 0.30000001
    1.0   / 3.0  == 0.30000000000000004
    1.0df / 3.df == %.7Hf   # This does not
    1.0dd / 3.dd == %.16Df  # work (yet).
    user@debian:~$

    Further details can be found in publicly available PDF versions of the latest draft of IEEE 754-2019.

    Regrettably, while GCC comes with (rudimentary) support for _Decimal types, the standard library as of yet doesnʼt allow them to be printed onto the screen: http://tinyurl.com/3se32p4z

    »GCC does not provide the C library functionality associated with math.h, fenv.h, stdio.h, stdlib.h, and wchar.h, which must come from a separate C library implementation. Because of this the GNU C compiler does not define macro __STDC_DEC_FP__ to indicate that the implementation conforms to the technical report.«

    Supposedly this is fixable by installing a library named “dfp” (from the "libdfp-dev" package). However, this only seems to work, if the GCC has been compiled with the –enable-decimal-float switch enabled (which isnʼt the case on my system).

  3. Small correction: during development of the above example, I switch from “1. / 3.” to “0.1 + 0.2”, but forgot to change the final printf() calls accordingly—hereʼs how they should read:

      /* … and print the result: */
      fprintf (stdout, "0.1f + 0.2f == %.8f\n", f32);
      fprintf (stdout, "0.1 + 0.2 == %.17lf\n", f64);

      fprintf (stdout, "0.1df + .2df == %.7Hf\n", df32);
      fprintf (stdout, "0.1dd + .2dd == %.16Df\n", df64);

  4. Thank you for the corrections. I don’t know what they did, so I just copied the material above from another source. That was the only information I could find. I appreciate the good details you added. Thanks!

  5. My pleasure. Truth be told, I stumbled over these decimal floating-point variants somewhat by accident—every so often I check if new standards documents (pertaining to those CS topics that I tend to follow more closely) have been released.

    When I saw that there was a new IEEE-754 floating-point standard, I couldnʼt really help myself but to buy a copy

  6. Last addition from my side—promise 😉

    Luckily, it turns out that I was wrong: it is possible to print out _Decimal values in programs compiled with current versions of GCC¹—after #define-ing __STDC_WANT_DEC_FP__ (which I did), printf() calls can use the new format characters (like %Hf or %Df) if one installs—as well as references and links—the aforementioned dfp library:

    user@debian:~$ sudo apt install -y libdfp-dev
    Reading package lists... Done
    Building dependency tree... Done
    Reading state information... Done
    libdfp-dev is already the newest version (1.0.16-1+b1).
    0 upgraded, 0 newly installed, 0 to remove and 4 not upgraded.
    user@debian:~$
    user@debian:~$ gcc -std=c2x -o decfltp decfltp.c -Wall -Wextra -I/usr/include/dfp -ldfp
    user@debian:~$
    user@debian:~$ ./decfltp
    0.1f + 0.2f == 0.30000001
    0.1 + 0.2 == 0.30000000000000004
    0.1df + .2df == 0.3000000
    0.1dd + .2dd == 0.3000000000000000
    user@debian:~$

    Further details can be found in the following blog post titled GNU/Linux Decimal Floating Point with GCC, libdfp, and Glibc, which also contains the following interesting bit:

    »[…] there are two major storage types for decimal FP: the BID (binary integer decimal) and the DPD (densely packed decimal). Only the BID type is relevant on Intel-based machines and is the default type for x86_64 GCC (cf. Wikipedia). Hence, only the BID type will be discussed here. The DPD type is exclusive to hardware-based decimal FP on IBM mainframes.«

    ¹ That being said, Clang really doesnʼt support _Decimal types right now (which is fine, as they are optional in C23):

    user@debian:~$ clang-15 -std=c2x -o decfltp decfltp.c -Wall -Wextra -I/usr/include/dfp -ldfp
    decfltp.c:21:3: error: GNU decimal type extension not supported
    decfltp.c:21:24: error: invalid suffix 'df' on floating constant
    decfltp.c:23:24: error: invalid suffix 'dd' on floating constant
    user@debian:~$

Leave a Reply