Strange result of using T1 = unsigned T2 in C++

102 Views Asked by At

The following code confuses me.

https://godbolt.org/z/WcMTYM1q7

#include <iostream>

using ll = long long;
using ull = unsigned ll;

int main() {
    ull x = 0;
    std::cout << x << std::endl;
}

This code fails to compile, in g++ 11, but successful to compile in g++ 12 and g++ 13. In g++ 11, compiler says:

error: ambiguous overload for 'operator<<' (operand types are 'std::ostream' {aka 'std::basic_ostream<char>'} and 'ull' {aka 'long unsigned int'})

And I also test the following code.

https://godbolt.org/z/PrePfoa6E

#include <iostream>

using ll = long long;
using ull = unsigned ll;
using ull2 = std::make_unsigned_t<ll>;

int main() {
    std::cout << typeid(ll).name() << std::endl;
    std::cout << typeid(ull).name() << std::endl;
    std::cout << typeid(ull2).name() << std::endl;
}

When using g++ 13 to compile the code, it prints x, j and y, which mean long long, unsigned int and unsigned long long. And when using g++ 11 to compile the code, it prints x, m and y, which mean long long, unsigned long and unsigned long long. All in all, unsigned ll is not the expected type.

Why does this happen? Is it a compiler bug? When should I use std::make_unsigned?

2

There are 2 best solutions below

0
user17732522 On BEST ANSWER

using ull = unsigned ll; is not valid standard C++. You can't add a type-specifier like unsigned to a typedef name like ll. That's only possible for cv-qualifiers, i.e. const and volatile, not other qualifiers that change the type like unsigned.

The compiler should issue a diagnostic for this code and GCC does correctly do so with -pedantic or -pedantic-errors.

That it compiles at all without these flags, but doesn't with them, indicates that it is likely an intended non-standard-conforming behavior to accept this declaration and in that case there is no reason to assume that unsigned ll would be unsigned long long absent documentation saying so.

The documentation mentions that this syntax was allowed in K&R C for typedefs, but not in ISO C. Presumably this is left over for K&R C compatibility where long long and unsigned long long don't exist and so this syntax seems to not be implemented as expected for these types. And the C++11 using declaration is basically just syntactical sugar for typedef, so the behavior was probably taken from the typedef behavior, even if it doesn't make sense for compatibility. -pedantic-errors enforces standards-conformance, so that this compatibility feature is not permitted anymore.

However, the overload resolution failure for GCC 11 looks unintended to me either way and could be explained as a bug fixed in the later versions.

0
Jan Schultke On

It is a compiler bug. I don't know why GCC compiles this, but unsigned ll definitely is ill-formed code. The failure in overload resolution is a consequence of allowing this declaration, since declaring ull normally as: unsigned, unsigned long, or unsigned long long all work. Declaring it as unsinged ll produces some sort of broken type that isn't truly any of these.

using ull = unsigned ll;

Clang rejects it as it should:

<source>:4:22: error: type-id cannot have a name
using ull = unsigned ll;
                     ^~

And so does MSVC:

<source>(4): error C2187: syntax error: 'll' was unexpected here

In general, unsigned can only be used in conjunction with (implicit) int in a specifier list. You have to use std::make_unsigned_t<ll>, otherwise this code is ill-formed.

You can still make GCC reject this by adding the flags:

-Wpedantic -Werror
# or
-pedantic-errors