109

I experienced some odd behavior while using C++ type traits and have narrowed my problem down to this quirky little problem for which I will give a ton of explanation since I do not want to leave anything open for misinterpretation.

Say you have a program like so:

#include <iostream>
#include <cstdint>

template <typename T>
bool is_int64() { return false; }

template <>
bool is_int64<int64_t>() { return true; }

int main()
{
 std::cout << "int:\t" << is_int64<int>() << std::endl;
 std::cout << "int64_t:\t" << is_int64<int64_t>() << std::endl;
 std::cout << "long int:\t" << is_int64<long int>() << std::endl;
 std::cout << "long long int:\t" << is_int64<long long int>() << std::endl;

 return 0;
}

In both 32-bit compile with GCC (and with 32- and 64-bit MSVC), the output of the program will be:

int:           0
int64_t:       1
long int:      0
long long int: 1

However, the program resulting from a 64-bit GCC compile will output:

int:           0
int64_t:       1
long int:      1
long long int: 0

This is curious, since long long int is a signed 64-bit integer and is, for all intents and purposes, identical to the long int and int64_t types, so logically, int64_t, long int and long long int would be equivalent types - the assembly generated when using these types is identical. One look at stdint.h tells me why:

# if __WORDSIZE == 64
typedef long int  int64_t;
# else
__extension__
typedef long long int  int64_t;
# endif

In a 64-bit compile, int64_t is long int, not a long long int (obviously).

The fix for this situation is pretty easy:

#if defined(__GNUC__) && (__WORDSIZE == 64)
template <>
bool is_int64<long long int>() { return true; }
#endif

But this is horribly hackish and does not scale well (actual functions of substance, uint64_t, etc). So my question is: Is there a way to tell the compiler that a long long int is the also a int64_t, just like long int is?


My initial thoughts are that this is not possible, due to the way C/C++ type definitions work. There is not a way to specify type equivalence of the basic data types to the compiler, since that is the compiler's job (and allowing that could break a lot of things) and typedef only goes one way.

I'm also not too concerned with getting an answer here, since this is a super-duper edge case that I do not suspect anyone will ever care about when the examples are not horribly contrived (does that mean this should be community wiki?).


Append: The reason why I'm using partial template specialization instead of an easier example like:

void go(int64_t) { }

int main()
{
    long long int x = 2;
    go(x);
    return 0;
}

is that said example will still compile, since long long int is implicitly convertible to an int64_t.


Append: The only answer so far assumes that I want to know if a type is 64-bits. I did not want to mislead people into thinking that I care about that and probably should have provided more examples of where this problem manifests itself.

template <typename T>
struct some_type_trait : boost::false_type { };

template <>
struct some_type_trait<int64_t> : boost::true_type { };

In this example, some_type_trait<long int> will be a boost::true_type, but some_type_trait<long long int> will not be. While this makes sense in C++'s idea of types, it is not desirable.

Another example is using a qualifier like same_type (which is pretty common to use in C++0x Concepts):

template <typename T>
void same_type(T, T) { }

void foo()
{
    long int x;
    long long int y;
    same_type(x, y);
}

That example fails to compile, since C++ (correctly) sees that the types are different. g++ will fail to compile with an error like: no matching function call same_type(long int&, long long int&).

I would like to stress that I understand why this is happening, but I am looking for a workaround that does not force me to repeat code all over the place.

jww
  • 97,681
  • 90
  • 411
  • 885
Travis Gockel
  • 26,877
  • 14
  • 89
  • 116
  • 1
    Out of curiosity, does your sample program give the same results for the `sizeof` each type? Perhaps the compiler is treating the size of `long long int` differently. – Blair Holloway Nov 12 '10 at 02:48
  • Have you compiled with C++0x enabled? C++03 doesn't have ``, so maybe the fact it has to say "this is an extension" (which it is) is foobaring it. – GManNickG Nov 12 '10 at 03:02
  • Yes, I should have probably specified that I'm using `--std=c++0x` . And yes, `sizeof(long long int) == sizeof(long int) == sizeof(int64_t) == 8`. – Travis Gockel Nov 12 '10 at 03:55
  • 1
    Nobody mentioned this yet, but in case it was overlooked: `long` and `long long` are distinct types (even if they have the same size & representation). `int64_t` is always an alias for another existing type (despite its name, `typedef` doesn't create new types, it just gives an alias to one that exists already) – M.M Mar 20 '15 at 20:35
  • 3
    One important statement is missing from the answers/comments, which helped me when this quirk hit me: *Never use fixed-size types for reliably specializing templates. Always use basic types and cover all possible cases* (even if you use fixed-size types to instantiate those templates). *All possible cases* means: if you need to instantiate with `int16_t`, then specialize with `short` and `int` and you'll be covered. (and with `signed char` if you're feeling adventurous) – Irfy Feb 02 '16 at 23:31

3 Answers3

54

You don't need to go to 64-bit to see something like this. Consider int32_t on common 32-bit platforms. It might be typedef'ed as int or as a long, but obviously only one of the two at a time. int and long are of course distinct types.

It's not hard to see that there is no workaround which makes int == int32_t == long on 32-bit systems. For the same reason, there's no way to make long == int64_t == long long on 64-bit systems.

If you could, the possible consequences would be rather painful for code that overloaded foo(int), foo(long) and foo(long long) - suddenly they'd have two definitions for the same overload?!

The correct solution is that your template code usually should not be relying on a precise type, but on the properties of that type. The whole same_type logic could still be OK for specific cases:

long foo(long x);
std::tr1::disable_if(same_type(int64_t, long), int64_t)::type foo(int64_t);

I.e., the overload foo(int64_t) is not defined when it's exactly the same as foo(long).

[edit] With C++11, we now have a standard way to write this:

long foo(long x);
std::enable_if<!std::is_same<int64_t, long>::value, int64_t>::type foo(int64_t);

[edit] Or C++20

long foo(long x);
int64_t foo(int64_t) requires (!std::is_same_v<int64_t, long>);
MSalters
  • 173,980
  • 10
  • 155
  • 350
  • 1
    Sad news is, e.g. on 64bit MSVC19 (2017) `sizeof()` `long` and `int` is identical, but `std::is_same::value` returns `false`. Same weirdness on AppleClang 9.1 on OSX HighSierra. – Ax3l Sep 06 '18 at 14:29
  • 3
    @Ax3l: That's not weird. Virtually every compiler since ISO C 90 has at least one such pair. – MSalters Sep 07 '18 at 07:07
  • That's true, they are distinct types. – Ax3l Sep 08 '18 at 08:54
7

Do you want to know if a type is the same type as int64_t or do you want to know if something is 64 bits? Based on your proposed solution, I think you're asking about the latter. In that case, I would do something like

template<typename T>
bool is_64bits() { return sizeof(T) * CHAR_BIT == 64; } // or >= 64
Logan Capaldo
  • 39,555
  • 5
  • 63
  • 78
  • 1
    Aren't you missing a `return` and a semicolon? – casablanca Nov 12 '10 at 03:03
  • Not what I'm looking for at all. The example was provided to show a way for the error to manifest itself, not as an actual requirement. – Travis Gockel Nov 12 '10 at 03:54
  • 1
    Still, you should be using `sizeof` for this. – Ben Voigt Nov 12 '10 at 04:34
  • No, I shouldn't. `template struct has_trivial_destructor : boost::false_type { }; template <> struct has_trivial_destructor : boost::true_type { };` Now `has_trivial_destructor` will be erroneously be a `boost::false_type`. That is an example of this problem manifesting itself that has nothing to do with variable size. – Travis Gockel Nov 12 '10 at 13:01
  • 5
    long long int and long int are not the same type whether or not they happen to be the same size. The behavior is not erroneous. That's just C++. – Logan Capaldo Nov 12 '10 at 14:26
  • While C++ sees them as different types, GCC's code generator treats them in the exact same way. I mean erroneous in the context usage, not in the context of the language. This brings me to my original question, which is: What is a good way to resolve this issue? I ask because I do not think there is one. – Travis Gockel Nov 12 '10 at 15:26
  • What issue? Are you trying to determine how the code generator behaves from a source code level? Or do you just not want to type `template <> struct trait { ... }` because you expect `template <> struct trait { ... }` to "cover" it? It's entirely possible for a compiler to have a `long long int` that is bigger than 64bits. This is the same "problem" you'd have with `int32_t` and `int` and `long int` on some compilers. `int64_t` isn't all possible 64 bit signed numeric builtin types, it is one of those built in types that satisfies the condition of being 64bits. – Logan Capaldo Nov 12 '10 at 15:46
  • I know. The underlying question (which I'm having a difficult time expressing) is really a question of the C++ type system. `int64_t` is just one example of this. There are many types which, at code generation time, are the *exact same* (like `long int` and `long long int` in 64-bit GCC), yet at C++ time, they are not recognized as the same type. This seems to be a fundamental limitation of C++'s type system -- how does one get around it in a clean(/not as ugly) way? – Travis Gockel Nov 12 '10 at 16:07
  • I don't think I'd call this a limitation. If you could get around it, it would be a violation of the principle of nominal type equality present in C++. For example, if specializing on `long int` counted as a specialization on `long long int`, then consider `struct A { int a; };` and `struct B { int a; };` Should specializing on A also apply to B? They will generate the same code after all. There _are_ languages that work like this, C++ just isn't one of them. – Logan Capaldo Nov 12 '10 at 21:03
  • Then would it be fair to say that this is a limitation of nominal typing? – Travis Gockel Nov 13 '10 at 00:37
  • Well it's intrisic to the whole idea, in C++ types are identified by their names. This also makes useful things like forward declaration pimpl and pointers type safe. In most C++ impls string* and int* are the same size for instance. A language with structural (sub)typing would have different limitations and tradeoffs. Lots of C++ template programming relies on tags (empty structs) which would stop working. This is one of those cake and eating it too things. – Logan Capaldo Nov 13 '10 at 00:55
  • 5
    It's not a limitation of nominal typing. It's a limitation of *meaningless* nominal typing. In the old days, the de facto standard was `short` = 16 bits, `long` = 32 bits, and `int` = native size. In these days of 64-bit, `int` and `long` don't *mean* anything anymore. – dan04 Nov 14 '10 at 01:56
  • 1
    @dan04: They're no more or less meaningful than they ever were. `short` is *at least* 16 bits, `int` is at least 16 bits, and `long` is at least 32 bits, with (sloppy notation follows) short <= int <= long. The "old days" you refer to never existed; there have always been variations within the restrictions imposed by the language. The "All the world's an x86" fallacy is just as dangerous as the older "All the world's a VAX fallacy. – Keith Thompson Sep 13 '11 at 19:05
4

So my question is: Is there a way to tell the compiler that a long long int is the also a int64_t, just like long int is?

This is a good question or problem, but I suspect the answer is NO.

Also, a long int may not be a long long int.


# if __WORDSIZE == 64
typedef long int  int64_t;
# else
__extension__
typedef long long int  int64_t;
# endif

I believe this is libc. I suspect you want to go deeper.

In both 32-bit compile with GCC (and with 32- and 64-bit MSVC), the output of the program will be:

int:           0
int64_t:       1
long int:      0
long long int: 1

32-bit Linux uses the ILP32 data model. Integers, longs and pointers are 32-bit. The 64-bit type is a long long.

Microsoft documents the ranges at Data Type Ranges. The say the long long is equivalent to __int64.

However, the program resulting from a 64-bit GCC compile will output:

int:           0
int64_t:       1
long int:      1
long long int: 0

64-bit Linux uses the LP64 data model. Longs are 64-bit and long long are 64-bit. As with 32-bit, Microsoft documents the ranges at Data Type Ranges and long long is still __int64.

There's a ILP64 data model where everything is 64-bit. You have to do some extra work to get a definition for your word32 type. Also see papers like 64-Bit Programming Models: Why LP64?


But this is horribly hackish and does not scale well (actual functions of substance, uint64_t, etc)...

Yeah, it gets even better. GCC mixes and matches declarations that are supposed to take 64 bit types, so its easy to get into trouble even though you follow a particular data model. For example, the following causes a compile error and tells you to use -fpermissive:

#if __LP64__
typedef unsigned long word64;
#else
typedef unsigned long long word64;
#endif

// intel definition of rdrand64_step (http://software.intel.com/en-us/node/523864)
// extern int _rdrand64_step(unsigned __int64 *random_val);

// Try it:
word64 val;
int res = rdrand64_step(&val);

It results in:

error: invalid conversion from `word64* {aka long unsigned int*}' to `long long unsigned int*'

So, ignore LP64 and change it to:

typedef unsigned long long word64;

Then, wander over to a 64-bit ARM IoT gadget that defines LP64 and use NEON:

error: invalid conversion from `word64* {aka long long unsigned int*}' to `uint64_t*'
jww
  • 97,681
  • 90
  • 411
  • 885