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_SOURCE
in all modes, anyway. If you’re manually passing-D_GNU_SOURCE
in 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
__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
, 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).