Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I always safely cast into the underlying type of a fixed (scoped) enumeration?

TL;DR: Is the following always safe? Or does it lead to undefined, unspecified or implementation defined behaviour?

template <class T> 
using ut = typename std::underlying_type<T>::type;

template <typename E> ut<E> identity(ut<E> value) {
  return static_cast<ut<E>>(static_cast<E>(value));
}

If I have a scoped enumeration I can always cast it into the underlying type:

#include <cassert>             // if you want to follow along
#include <initializer_list>    // copy everything and remove my text

enum class priority : int { 
  low = 0, 
  normal = 1,
  high = 2 
};

// works fine
int example = static_cast<int>(priority::high);

For all values that are defined in the enumeration I can also expect that I get the value back:

constexpr priority identity_witness(priority p) {
  return static_cast<priority>(static_cast<int>(p));
}

void test_enum() {
  for (const auto p : {priority::low, priority::normal, priority::high}) {
    assert(p == identity_witness(p));
  }
}

According to N3337 (C++11), 5.2.9 Static cast [expr.static.cast] § 9-10 this is fine:

  1. A value of a scoped enumeration type (7.2) can be explicitly converted to an integral type. The value is unchanged if the original value can be represented by the specified type. …
  2. A value of integral or enumeration type can be explicitly converted to an enumeration type. The value is unchanged if the original value is within the range of the enumeration values (7.2). …

However, I'm interested in the other way round. What happens if I cast to an enum and back to the underlying type?

constexpr int identity_witness(int i) {
  return static_cast<int>(static_cast<priority>(i));
}

void test_int() {
  for (const auto p : {0, 1, 2, 3, 4, 5}) {
    assert(p == identity_witness(p));
  }
}

int main() {
  test_enum();
  test_int();
}

This compiles and works fine, since a static_cast to the underlying type won't change the memory at all (probably). However, the standard says that the behaviour is unspecified if the value is not in the range:

  1. [continued] Otherwise, the resulting value is unspecified (and might not be in that range).

The range of the enumerations isn't clear to me. According to 7.2§7, "the values of the enumeration are the values of the underlying type" if the enumeration's underlying type is fixed. Therefore, for any std::underlying_type<my_enumeration_type>, the property above should hold.

Does this argument hold, or did I miss some strange clause in the standard so that a cast into the underlying type of the enumeration might lead to undefined or unspecified behaviour?

like image 990
Zeta Avatar asked Aug 03 '17 12:08

Zeta


People also ask

What happens when you perform a static_ cast?

Static Cast: This is the simplest type of cast which can be used. It is a compile time cast.It does things like implicit conversions between types (such as int to float, or pointer to void*), and it can also call explicit conversion functions (or implicit ones).

Can static cast fail?

As we learnt in the generic types example, static_cast<> will fail if you try to cast an object to another unrelated class, while reinterpret_cast<> will always succeed by "cheating" the compiler to believe that the object is really that unrelated class.

Why static_ cast is used in c++?

The static_cast operator can be used for operations such as converting a pointer to a base class to a pointer to a derived class. Such conversions are not always safe.

What is an unscoped enum type?

In an unscoped enum, the scope is the surrounding scope; in a scoped enum, the scope is the enum-list itself. In a scoped enum, the list may be empty, which in effect defines a new integral type. class. By using this keyword in the declaration, you specify the enum is scoped, and an identifier must be provided.


1 Answers

The standard seems determined to allow you to use arbitrary integral values of a given type as values for an enumeration fixed to that type, even if they're not named as enumeration values. The caveat in 5.2.9.10 is presumably meant to limit enumerations without a fixed underlying type. The standard doesn't define the "range" of a fixed-type enumeration's values as anything separate from the enumeration's values. In particular, it says:

It is possible to define an enumeration that has values not defined by any of its enumerators.

So "is within the range of the enumeration values" cannot be understood as anything other than "is one of the enumeration values". There's no other definition of the range of an enumeration's values.

So you're safe for enumerations with a fixed underlying type. For untyped enumerations, you're only safe if you stick to the safe number of bits.

like image 160
Sneftel Avatar answered Oct 04 '22 15:10

Sneftel