Is __int128 integral? A survey

How well is __int128 supported, and what are its properties? A Godbolt-based survey of the Big Four compilers. Spoiler alert: libc++ thinks it’s integral, libstdc++ thinks it’s integral only in -std=gnu++XX mode(!), and MSVC thinks it doesn’t exist at all.

Many thanks to Rein Halbersma for alerting me to libstdc++’s different behavior in -std=gnu++XX mode versus -std=c++XX mode. (UPDATE, 2021-07-16: And to Jonathan Wakely for alerting me to libstdc++ 10.3’s new behavior!)

Compiler support

using I128 = __int128;

GCC, Clang, and Intel ICC all support a built-in __int128 type. Microsoft MSVC does not support any 128-bit integral type as far as I’m aware.

On GCC, Clang, and ICC, __int128 is a token similar to int: you can modify it with unsigned to produce unsigned __int128. However, all three front-ends also support the built-in synonyms __int128_t and __uint128_t.

static_assert(std::is_same_v<__int128_t, __int128>);
static_assert(std::is_same_v<__uint128_t, unsigned __int128>);
    // PASSES on all three compilers

Both __int128_t and __uint128_t are safe to use in non-type template parameters and constant expressions, on all compilers that support them.

Sneaky macros

The story on libstdc++ is complicated. Libstdc++ doesn’t do anything special for __int128, but it does provide macro hooks (link) so that for example __GLIBCXX_TYPE_INT_N_0 will be treated as an integral type, whatever that macro expands to. (If it doesn’t expand to anything, it’ll be ignored.)

If you compile with GCC in -std=c++XX mode (for any XX), __GLIBCXX_TYPE_INT_N_0 won’t be defined and so libstdc++ will consider __int128 a non-integral type.

But if you compile with GCC in -std=gnu++XX mode (for any XX), then the compiler will pre-define these two macros:

#define __GLIBCXX_BITSIZE_INT_N_0 128
#define __GLIBCXX_TYPE_INT_N_0 __int128

As of July 2016-ish, Clang also defines these two macros in -std=gnu++XX mode.

You might wonder if these macros have any relationship with _GNU_SOURCE: They do not. Both Clang and GCC unconditionally define _GNU_SOURCE in all modes, anyway. If you’re manually passing -D_GNU_SOURCE in your CFLAGS, like a lot of projects I’ve seen, well, you can stop doing that.

By the way, you can use the options -dM -E to view all your compiler’s pre-defined macros:

clang++ -std=c++17 -dM -E test.cpp

The upshot is, if you compile in -std=gnu++XX mode, libstdc++ will treat __int128 and unsigned __int128 as integral types with the proper numeric limits; but if you compile in the usual -std=c++XX mode, libstdc++ will not do any of that.

On the other hand, libc++ treats __int128 as an integer type, full stop.

Relationship to integer types and intmax_t

static_assert(std::is_integral_v<__int128>);
    // TRUE on libc++, "it depends" on libstdc++

libstdc++ (in standard, non-gnu++XX mode) leaves is_integral_v<__int128> as false. This makes a certain amount of sense from the library implementor’s point of view, because __int128 is not one of the standard integral types, and furthermore, if you call it integral, then you have to face the consequence that intmax_t (which is 64 bits on every ABI that matters) is kind of lying about being the “max.”

In -std=gnu++XX mode, libstdc++ makes is_integral_v<__int128> come out to true, as described in the previous section on sneaky macros.

Meanwhile, libc++ simply sets sets is_integral_v<__int128> to true, unconditionally.

Yes, this means that on libc++ (or on libstdc++ with -std=gnu++17), there exist integral values that do not fit in intmax_t! (Godbolt.)

In case you’re wondering: libstdc++ in standard C++17 mode considers __int128 a “compound” type. The definition of “compound type” is simply “anything that’s not a fundamental type,” where “fundamental” encompasses the arithmetic types (both integral and floating-point), void, and nullptr_t. So for example int* is also a compound type.

Numeric limits

Prior to the GCC 10.3 release, libstdc++ specialized numeric_limits<__int128> only in non-gnu++XX mode. This had the unfortunate effect of

constexpr __int128 int128_max =
    std::numeric_limits<__int128>::max();
static_assert(int128_max == 0);  // TRUE on libstdc++ in std mode before 10.3

However, since GCC 10.3 (specifically this August 2020 commit), libstdc++ has provided the appropriate specialization in all modes, just like libc++, so that numeric_limits<__int128>::min() and numeric_limits<__int128>::max() have the appropriate values instead of zero.

Naturally, numeric_limits<__int128>::is_integer is true whenever the specialization is provided at all — i.e., unconditionally on libc++ and libstdc++ 10.3+, and on older libstdc++ in -std=gnu++XX mode. So:

static_assert(std::numeric_limits<__int128>::is_integer);  // TRUE everywhere since August 2020
static_assert(std::is_integral_v<__int128>);  // still FALSE on libstdc++ in std mode

Signedness

libc++ reports that is_signed_v<__int128> and is_unsigned_v<unsigned __int128>. libstdc++ in standard mode of course denies both, because only arithmetic types can be signed or unsigned, and libstdc++ denies that __int128 is arithmetic.

As for make_signed, libc++ (and libstdc++ in gnu++XX mode) reports what you’d expect — make_signed<__uint128_t>::type is __int128_t, and make_unsigned<__int128_t>::type is __uint128_t.

Trying to instantiate make_signed<__uint128_t> on libstdc++ in standard mode results in a hard error which is not SFINAE-friendly. (It’s undefined behavior to instantiate make_signed of any type which is neither integral nor an enumeration type.)

Incidentally, today I learned that is_signed_v<float> == true but make_signed_t<float> is a hard error.

Incrementability

UPDATE, 2021-07-22: Jonathan Wakely alerted me to some new wrinkles in C++20. C++20 gives us at least two relevant library concepts: std::integral and std::incrementable.

std::integral is true of integral types, i.e., std::integral<T> is true iff std::is_integral_v<T> is true. On libc++ (or libstdc++ in -std=gnu++XX mode) they’re both true; on libstdc++ in -std=c++XX mode they’re both false.

std::incrementable refines std::weakly_incrementable, which is modeled by integer-like types and also by pointer-like, iterator-like types. It is significant because if a type does not satisfy std::weakly_incrementable, then (by definition) it is not integer-like, and that means that it can’t be used as any iterator’s difference_type. Since we certainly want to be able to use __int128 as the difference_type of, say, iota_view<__int128>, it’s important that __int128 be considered “integer-like.” Therefore, libstdc++ in -std=c++XX mode specializes C++20’s std::incrementable_traits for both __int128_t and __uint128_t.

On libc++, and libstdc++ in -std=gnu++XX mode, __int128 is automatically std::incrementable by virtue of being std::integral; it doesn’t need those additional specializations. (Note that std::integral does not subsume std::incrementable; that’s another story.)


For more on __uint128_t arithmetic, see “The abstraction penalty for wide integer math on x86-64” (2020-02-13).

Posted 2019-02-28