Is __int128 integral? A survey
__int128 integral? A surveyHow 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_SOURCEin all modes, anyway. If you’re manually passing-D_GNU_SOURCEin yourCFLAGS, 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
__int128a “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, andnullptr_t. So for exampleint*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).
