Weirdness with enum promotion and underlying_type

347 views
Skip to first unread message

Myriachan

unread,
Jun 20, 2014, 9:53:16 PM6/20/14
to std-dis...@isocpp.org
I noticed that GCC and clang in C++11 mode act weirdly when using underlying_type on unscoped, unfixed-type enums.  If the enum in question has no values outside the range [0...INT_MAX], these compilers will promote these enums to type "signed int" when the situation calls for it.  However, if you use std::underlying_type, or the compiler intrinsic equivalent, the type returned is "unsigned int".

It seems as though this is compliant with the Standard, but it is rather counter-intuitive.  Things would make much more sense if promote(E) acted the same as promote(underlying_type(E)).  The compiler ought to have to make a choice of underlying type and use it for both promotion and explicit use of std::underlying_type.  Is there some reason that this odd behavior is allowed?

The following code has its second static_assert fire on GCC 4.8.4 and clang 3.4, but not Visual Studio 2013.  (MSVS 2013 apparently chooses "signed int" for both, an equally-valid choice.)  Note that all three compilers implement std::underlying_type as __underlying_type.

enum unscoped_unfixed_size_enum {
        some_enum_name = 0,
};

static_assert(some_enum_name > -1, "some_enum_name is unsigned?!");
static_assert(static_cast<__underlying_type(unscoped_unfixed_size_enum)>(some_enum_name) > -1,
        "__underlying_type(some_enum_name) is unsigned?!");

Melissa

David Krauss

unread,
Jun 20, 2014, 10:06:35 PM6/20/14
to std-dis...@isocpp.org

On 2014–06–21, at 9:53 AM, Myriachan <myri...@gmail.com> wrote:

> I noticed that GCC and clang in C++11 mode act weirdly when using underlying_type on unscoped, unfixed-type enums. If the enum in question has no values outside the range [0...INT_MAX], these compilers will promote these enums to type "signed int" when the situation calls for it. However, if you use std::underlying_type, or the compiler intrinsic equivalent, the type returned is "unsigned int".
>
> It seems as though this is compliant with the Standard, but it is rather counter-intuitive.

Indeed. So what’s the problem?

Use scoped enumerations instead.

> Things would make much more sense if promote(E) acted the same as promote(underlying_type(E)). The compiler ought to have to make a choice of underlying type and use it for both promotion and explicit use of std::underlying_type. Is there some reason that this odd behavior is allowed?

Not allowed, required. The main reason is C compatibility.

> The following code has its second static_assert fire on GCC 4.8.4 and clang 3.4, but not Visual Studio 2013. (MSVS 2013 apparently chooses "signed int" for both, an equally-valid choice.) Note that all three compilers implement std::underlying_type as __underlying_type.
>
> enum unscoped_unfixed_size_enum {
> some_enum_name = 0,
> };
>
> static_assert(some_enum_name > -1, "some_enum_name is unsigned?!");
> static_assert(static_cast<__underlying_type(unscoped_unfixed_size_enum)>(some_enum_name) > -1,
> "__underlying_type(some_enum_name) is unsigned?!”);

Unscoped enumerations are unpredictable like that.

Myriachan

unread,
Jun 20, 2014, 11:54:30 PM6/20/14
to std-dis...@isocpp.org


On Friday, June 20, 2014 7:06:35 PM UTC-7, David Krauss wrote:

Indeed. So what’s the problem?

Use scoped enumerations instead.  

1. This came up trying to make a type-safe *printf via varargs templates.  (Not everyone likes streams.)  The trouble was simulating the promotion effects of passing arguments to varargs functions.
2. It was with enums that existed in system-supplied C headers that I first noticed this weirdness.
3. Scoped enums aren't the solution to everything; they're very annoying sometimes when you *want* them to go into integer variables.  Fixed-type enums that aren't scoped are considerably easier to work with this way.
 
> Things would make much more sense if promote(E) acted the same as promote(underlying_type(E)).  The compiler ought to have to make a choice of underlying type and use it for both promotion and explicit use of std::underlying_type.  Is there some reason that this odd behavior is allowed?

Not allowed, required. The main reason is C compatibility.

Under what conditions would C ever have an observable difference, given that there is no std::underlying_type?  Putting an unsigned int and an enum into a union on a non-two's-complement architecture?

Unscoped enumerations are unpredictable like that.

I should try to find a non-two's-complement architecture C++11 compiler and see what happens.  It seems broken that this union would have undefined behavior - a and b ought to have the same bit representation, since that is what "underlying_type" is supposed to mean.


enum unscoped_unfixed_size_enum {
        some_enum_name = 0,
};

union broken_union {
        unscoped_unfixed_size_enum a;
        std::underlying_type<unscoped_unfixed_size_enum>::type b;
};

David Krauss

unread,
Jun 21, 2014, 12:42:38 AM6/21/14
to std-dis...@isocpp.org
On 2014–06–21, at 11:54 AM, Myriachan <myri...@gmail.com> wrote:



On Friday, June 20, 2014 7:06:35 PM UTC-7, David Krauss wrote:

Indeed. So what’s the problem?

Use scoped enumerations instead.  

1. This came up trying to make a type-safe *printf via varargs templates.  (Not everyone likes streams.)  The trouble was simulating the promotion effects of passing arguments to varargs functions.
2. It was with enums that existed in system-supplied C headers that I first noticed this weirdness.
3. Scoped enums aren't the solution to everything; they're very annoying sometimes when you *want* them to go into integer variables.  Fixed-type enums that aren't scoped are considerably easier to work with this way.

I prefer to define a generic unary operator+ enumeration promotion operator. The one extra character is worth avoiding the toxicity of unscoped enumerations.

If you SFINAE-disable this operator+ overload when +E() is already well-formed without it, then you can make +e  always perform varargs-like promotion. This is easier said than done, but I think it’s possible.

Much simpler to use a user-defined trait class as the SFINAE predicate instead. This is what I do, but it does require annotating every scoped enumeration that the operator should apply to.

Under what conditions would C ever have an observable difference, given that there is no std::underlying_type?  Putting an unsigned int and an enum into a union on a non-two's-complement architecture?

Promotions are still observable even without std::underlying_type. C++ since C++98 specifies that an (unscoped) enumeration value implicitly converts to the lowest-rank promoted integer type that can represent all the enumerators (§4.5/2 or 3), regardless of the underlying type (which already existed in C++98 §7.2/5). C makes the converted-to type implementation-defined, not unlike the C++ underlying type. This can be directly observed as of C11 using a type-generic expression and the unary + operator.

Making the underlying type identical to the C++ implicit promotion conversion type is a very reasonable policy, but for whatever reason, Microsoft did not choose to do so.

Essentially your proposal comes down to requiring MSVC to change its semantics to hide this blunder. Since the impact on users is the same, it might be more fruitful to complain directly to Microsoft, and see if they can change their underlying types, at least as far as std::underlying_type goes if not the ABI.

Unscoped enumerations are unpredictable like that.

I should try to find a non-two's-complement architecture C++11 compiler and see what happens.  It seems broken that this union would have undefined behavior - a and b ought to have the same bit representation, since that is what "underlying_type" is supposed to mean.

Enumerations are not aliasing-compatible with their underlying types (although wide character types are), so you can’t use such a union directly nor the equivalent reinterpret_cast. This has been brought up in undefined behavior discussions. Anyway, I wasn’t talking about bitwise representations.

Thiago Macieira

unread,
Jun 21, 2014, 1:05:14 AM6/21/14
to std-dis...@isocpp.org
Em sex 20 jun 2014, às 18:53:16, Myriachan escreveu:
> The following code has its second static_assert fire on GCC 4.8.4 and clang
> 3.4, but not Visual Studio 2013. (MSVS 2013 apparently chooses "signed
> int" for both, an equally-valid choice.)

Try visual studio with the -Za option. Behaviour changes.

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center
PGP/GPG: 0x6EF45358; fingerprint:
E067 918B B660 DBD1 105C 966C 33F5 F005 6EF4 5358

Myriachan

unread,
Jun 21, 2014, 5:27:56 PM6/21/14
to std-dis...@isocpp.org
On Friday, June 20, 2014 9:42:38 PM UTC-7, David Krauss wrote:
>
> Promotions are still observable even without std::underlying_type. C++ since C++98 specifies that an (unscoped) enumeration value implicitly converts to the lowest-rank promoted integer type that can represent all the enumerators (§4.5/2 or 3), regardless of the underlying type (which already existed in C++98 §7.2/5). C makes the converted-to type implementation-defined, not unlike the C++ underlying type. This can be directly observed as of C11 using a type-generic expression and the unary + operator.
>
> Making the underlying type identical to the C++ implicit promotion conversion type is a very reasonable policy, but for whatever reason, Microsoft did not choose to do so.
>
> Essentially your proposal comes down to requiring MSVC to change its semantics to hide this blunder. Since the impact on users is the same, it might be more fruitful to complain directly to Microsoft, and see if they can change their underlying types, at least as far as std::underlying_type goes if not the ABI.
>
> Unscoped enumerations are unpredictable like that.
>

Sorry; I'm a bit confused here.

I thought that "signed int" was "the lowest rank promoted integer type that can represent all the enumerators", given that the only enumerator is 0. In Microsoft's compiler, the promoted type is "signed int" and the underlying_type is "signed int", so wouldn't that mean that they are already following "a very reasonable policy" of keeping the two types identical?

It's GCC and clang that are setting the promoted type and underlying type to different things.

>
> Enumerations are not aliasing-compatible with their underlying types (although wide character types are), so you can’t use such a union directly nor the equivalent reinterpret_cast. This has been brought up in undefined behavior discussions. Anyway, I wasn’t talking about bitwise representations.

The compilers in widespread use all allow type punning through a union, because the true, official constraint in the Standard - that memcpy() is the only way out - is far too much of an impediment to getting work done and to machine performance.

(A might_alias statement would be really useful in C/C++. Declare that some number of pointers and/or references may point to the same elements even if their types are unrelated.)

Melissa

David Krauss

unread,
Jun 22, 2014, 10:36:35 PM6/22/14
to std-dis...@isocpp.org

On 2014–06–22, at 5:27 AM, Myriachan <myri...@gmail.com> wrote:

> I thought that "signed int" was "the lowest rank promoted integer type that can represent all the enumerators", given that the only enumerator is 0. In Microsoft's compiler, the promoted type is "signed int" and the underlying_type is "signed int", so wouldn't that mean that they are already following "a very reasonable policy" of keeping the two types identical?
>
> It's GCC and clang that are setting the promoted type and underlying type to different things.

Oh, I got it backward then. My argument still works, it’s just at my expense now :P .

The System V AMD64 ABI, Figure 3.1, defines the preferred underlying type as int, “bumped to an unsigned int” as “permitted” by the C/C++ implementation, so something is fishy.

>> Enumerations are not aliasing-compatible with their underlying types (although wide character types are), so you can’t use such a union directly nor the equivalent reinterpret_cast. This has been brought up in undefined behavior discussions. Anyway, I wasn’t talking about bitwise representations.
>
> The compilers in widespread use all allow type punning through a union, because the true, official constraint in the Standard - that memcpy() is the only way out - is far too much of an impediment to getting work done and to machine performance.

-fstrict-aliasing is on by default in GCC. It was a very contentious change but the user base seems to have mostly adapted by now (or at least learned to pass -fnostrict-aliasing).

> (A might_alias statement would be really useful in C/C++. Declare that some number of pointers and/or references may point to the same elements even if their types are unrelated.)

I wish I were expert enough to know why this isn’t already done. In C++, it would be nice as a local object and the aliasing potential exists as long as it’s in scope.

Reply all
Reply to author
Forward
0 new messages